如果你不控制所涉及的类型,你如何重构“打开类型”是多态的?

时间:2009-02-16 06:39:15

标签: c# generics oop reflection

我正在用C#编写一个microformats解析器,我正在寻找一些重构建议。这可能是我在C#中尝试了一段时间的第一个“真正的”项目(我在日常工作中几乎完全用VB6编程),所以我觉得这个问题可能成为系列中的第一个; - )

让我提供一些关于我到目前为止的背景,以便我的问题(希望)有意义。

现在,我有一个班级,MicroformatsParser,完成所有工作。它有一个重载的构造函数,允许您传递包含URI的System.Uristring:在构建时,它会在给定的URI下载HTML文档并将其加载到HtmlAgilityPack.HtmlDocument中以方便班级操纵。

基本API就像这样(或者,一旦我完成代码......):

MicroformatsParser mp = new MicroformatsParser("http://microformats.org");
List<HCard> hcards = mp.GetAll<HCard>();

foreach(HCard hcard in hcards) 
{
    Console.WriteLine("Full Name: {0}", hcard.FullName);

    foreach(string email in hcard.EmailAddresses)
        Console.WriteLine("E-Mail Address: {0}", email);
}

这里使用仿制药是故意的。我从Firefox 3中的Microformats库的工作方式(以及Ruby mofo gem)中获得灵感。这里的想法是解析器完成繁重的工作(在HTML中查找实际的微格式内容),微格式类本身(上例中的HCard)基本上提供了一个模式,告诉解析器如何处理找到的数据。

HCard类的代码应该更清楚(注意这不是一个完整的实现):

[ContainerName("vcard")]
public class HCard
{
    [PropertyName("fn")]
    public string FullName;

    [PropertyName("email")]
    public List<string> EmailAddresses;

    [PropertyName("adr")]
    public List<Address> Addresses;

    public HCard()
    { 
    }
}

解析器使用此处的属性来确定如何使用HTML文档中的数据填充类的实例。当您致电GetAll<T>()时,解析器会执行以下操作:

  • 检查类型T是否具有ContainerName属性(并且不是空白)
  • 在HTML文档中搜索class属性与ContainerName匹配的所有节点。将这些称为“容器节点”。
  • 对于每个容器节点:
    • 使用反射创建T类型的对象。
    • 通过反映
    • 获取类型MemberInfo[]的公开字段(T
    • 对于每个字段的MemberInfo
      • 如果该字段具有PropertyName属性
        • 从HTML
        • 获取相应微格式属性的值
        • 将HTML中找到的值注入字段(即设置第一步中创建的T类型对象上的字段值)
        • T类型的对象添加到List<T>
    • 返回List<T>,现在包含一堆微格式对象

我正在尝试找出一种更好的方法来实现粗体中的步骤。问题是,微格式类中给定字段的Type不仅决定了HTML中要查找的节点,还决定了如何解释数据。

例如,返回上面定义的HCard类,"email"属性绑定到EmailAddresses字段,即List<string>。解析器在HTML中找到父"email"节点的所有"vcard"子节点后,必须将它们放在List<string>中。

更重要的是,如果我希望我的HCard能够返回电话号码信息,我可能希望能够声明List<HCard.TelephoneNumber>类型的新字段(具有自己的ContainerName("tel") { {1}}属性)保存该信息,因为HTML中可以有多个"tel"元素,"tel"格式有自己的子属性。但现在解析器需要知道如何将电话数据放入List<HCard.TelephoneNumber>

同样的问题适用于Float S,DateTime S,List<Float> S,List<Integer> S等。

显而易见的答案是让解析器切换字段类型,并针对每种情况进行适当的转换,但我想避免使用巨大的switch语句。请注意,我不打算让解析器支持所有可能存在的Type,但我希望它能够处理大多数标量类型及其List<T>版本以及识别能力其他微格式类(以便微格式类可以由其他微格式类组成)。

有关如何最好地处理此事的任何建议?

由于解析器必须处理原始数据类型,我认为我不能在类型级别添加多态性...

我的第一个想法是使用方法重载,所以我会有一系列GetPropValue重载,例如GetPropValue(HtmlNode node, ref string retrievedValue)GetPropValue(HtmlNode, ref List<Float> retrievedValue)等,但我想知道是否有更好地解决这个问题。

3 个答案:

答案 0 :(得分:5)

Mehrdad的方法基本上是我建议的方法,但作为可能更多的第一步。

对于单个类型(字符串,基元等),您可以使用简单的IDictionary<Type,Delegate>(其中每个条目实际上从TFunc<ParseContext,T> - 但不能用泛型表示) )但是你还要检查列表,地图等。你将无法使用地图执行此操作,因为您必须为每种类型的列表都有一个条目(即{的单独条目{1}},List<string>等)。泛型使这非常棘手 - 如果你很乐意将自己局限于只是某些具体类型,例如List<int>,你会让自己更容易(但不那么灵活)。例如,检测List<T>非常简单:

List<T>

检测类型是否为某些if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { // Handle lists // use type.GetGenericArguments() to work out the element type } 实现IList<T>(然后发现T)可能会很痛苦,特别是因为可能存在多个实现,并且具体类型本身可能是可能不是通用的。如果您真的需要成千上万开发人员使用的非常灵活的库,那么这项工作可能是值得的 - 但我会保持简单。

答案 1 :(得分:4)

您可以构建一个字典,将字符串映射到委托,而不是使用大型的switch语句,并在需要使用适当的方法进行解析时查找它。

答案 2 :(得分:2)

这与我的序列化引擎(protobuf-net)面临的问题非常类似。我简单地将其分解为常见的逻辑集 - IList<T>等(尽管有一个很大的逻辑/类型测试来处理各种基元)。我使用的方法是:只做一次...构建一个基于接口/基类的模型,可以处理不同类型的属性,并从那里开始工作。我在通用缓存类的静态初始化程序中执行此操作 - 即Foo<T>;当T是HCard时,我预先计算了一个模型(即我创建了一个可以解析/渲染该属性并存储它们的每个属性的对象),这使我可以在不再考虑之后再处理HCard上。

我不是说这是世界上最好的代码,但似乎工作正常。