c#方法能够处理/接受不同的类类型

本文关键字:类型 方法 处理 | 更新日期: 2023-09-27 18:15:20

我有一个方法,它接受一个简单的类对象并构建一个API调用中使用的URL。我希望这个方法能够处理/接受不同的类类型是相似的,但有不同的属性。

public class ClientData
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...
}
public class PaymentData
{
   public decimal PaymentAmount {get; set;}
   public string Description {get; set;}
   ...
}

下面是两个示例方法。正如你所看到的,它们非常相似。是将这些实现为接受不同参数的不同方法更好,还是可以编写一个可以处理参数对象差异的方法?

public string BuildApiCall(ClientData clientDataObject)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append("name=" + clientDataObject.Name);
  sb.append("email=" + clientDataObject.Email);
  return sb.ToString();
}
public string BuildApiCall(PaymentData paymentDataObject)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append("payment=" + paymentDataObject.PaymentAmount );
  sb.append("description=" + paymentDataObject.Description );
  return sb.ToString();
}

c#方法能够处理/接受不同的类类型

决定采用哪种方法

你的问题,本质上是,为你的类创建一个自定义序列化器,基于所提供的API(大概是固定的)。

为了尽可能地分离关注点,此功能通常与实体类分开实现,将它们(如果可能的话)尽可能地保留为poco,或与序列化无关的哑dto。因此,就像使用XmlSerializerDataContractSerializer将类序列化为XML或Protobuf一样。NET将其序列化到协议缓冲区中,可以说最通用的方法是创建自己的序列化器。

当然,与您在日常编程中遇到的所有其他问题一样,您需要权衡潜在的好处,并决定您希望在重构中投入多少精力。如果情况很少,那么没有人会因为复制/粘贴几个硬编码方法而受到伤害,这与您现在所做的类似。同样,如果这只是一个小的"宠物项目",那么您可能不想在尝试重构成更通用的解决方案(您可能再也不需要了)时,在可能遇到的潜在问题上浪费时间。

你的目标是要写得尽可能少

然而,如果确实选择花一些时间来编写序列化器,那么您很快就会注意到,大多数序列化框架都试图尽可能地依赖约定进行序列化。换句话说,如果你的类是:
public class ClientData
{
    public string Name { get; set; }
    public string Email { get; set; }
}

那么XmlSerializer将在没有任何配置的情况下产生以下XML:

<ClientData>
    <Name>...</Name>
    <Email>...</Email>
</ClientData>

如果有一个类可以简单地为该对象输出?name=...&email=...,而完全不需要您做额外的工作,那将是非常酷的。如果这样做有效,那么你就有了一个类,它不仅可以从现有代码中删除重复,而且还可以为将来对API的所有扩展节省时间。

因此,如果您基于API编写类,那么尽可能将属性命名为与API成员完全相同(并使用基于约定的序列化)可能是有意义的,但仍然保持它足够开放,以便能够分别处理几个边缘情况。

示例代码

public class ClientData
{
    public string Name {get; set;}
    public string Email {get; set;}
}
// customer really insisted that the property is
// named `PaymentAmount` as opposed to simply `Amount`,
// so we'll add a custom attribute here
public class PaymentData
{
    [MyApiName("payment")]
    public decimal PaymentAmount {get; set;}
    public string Description {get; set;}
}

MyApiName属性非常简单,只接受一个字符串参数:

public class MyApiNameAttribute : Attribute
{
    private readonly string _name;
    public string Name
    { get { return _name; } }
    public MyApiNameAttribute(string name)
    { _name = name; }
}

设置好后,我们现在可以使用一些反射来呈现查询:

public static string Serialize(object obj)
{
    var sb = new StringBuilder();
    foreach (var p in obj.GetType().GetProperties())
    {
        // default key name is the lowercase property name
        var key = p.Name.ToLowerInvariant();
        // we need to UrlEncode all values passed to an url
        var value = Uri.EscapeDataString(p.GetValue(obj, null).ToString());
        // if custom attribute is specified, use that value instead
        var attr = p
            .GetCustomAttributes(typeof(MyApiNameAttribute), false)
            .FirstOrDefault() as MyApiNameAttribute;
        if (attr != null)
            key = attr.Name;
        sb.AppendFormat(
            System.Globalization.CultureInfo.InvariantCulture,
            "{0}={1}&",
            key, value);
    }
    // trim trailing ampersand
    if (sb.Length > 0 && sb[sb.Length - 1] == '&')
        sb.Length--;
    return sb.ToString();
}

用法:

var payment = new PaymentData()
{
    Description = "some stuff",
    PaymentAmount = 50.0m 
};
// this will produce "payment=50.0&description=some%20stuff"            
var query = MyApiSerializer.Serialize(payment)

性能

正如在注释中所指出的,反射的能力确实会导致性能损失。在大多数情况下,这不应该引起太大的关注。在这种情况下,如果您比较构建查询字符串的成本(可能在10微秒的范围内)与执行HTTP请求的成本,您会发现它几乎可以忽略不计。

然而,如果你决定要优化,你可以很容易地在最后,在分析之后,通过改变单一的方法,通过缓存属性信息甚至编译委托来完成所有的工作。这是关注点分离的好处;重复的代码很难优化。

定义接口:

public interface IWhatsit
{
     string ToApiString();
}

现在让数据对象实现它。ToApiString应该返回这个特定对象的查询字符串部分:

public class ClientData : IWhatsit
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...
   public string ToApiString()
   {
       // Do whatever you need here - use a string builder if you want
       return string.Format("Name={0}&Email={1}",Name,Email);
   }
}

现在你可以有一个单一的方法来进行API调用:

public string BuildApiCall(IWhatsit thing)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append(thing.ToApiString());
  return sb.ToString();
}

注意:如果你愿意,你可以在接口中使用属性而不是方法。

另一种方法是使用抽象基类并从中继承。然后你可以这样做:

public abstract class BaseData
{
    protected abstract string ToApiString();
    public string BuildApiCall()
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("http://mytestapi.com/");
        sb.append(ToApiString());
        return sb.ToString();
    }
}

然后每个类看起来像这样:

public class ClientData : BaseData
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...
   protected override string ToApiString()
   {
       // Do whatever you need here - use a string builder if you want
       return string.Format("Name={0}&Email={1}",Name,Email);
   }
}

允许您将BuildApiCall放入类本身并具有基本实现。当然,如果您确实需要BuildApiCall在这些类之外,那么您可以这样做。它只需要一个BaseData,你必须把ToApiString设为公共而不是保护。