我正在用C#编写一个microformats解析器,我正在寻找一些重构建议。这可能是我在C#中尝试了一段时间的第一个“真正的”项目(我在日常工作中几乎完全用VB6编程),所以我觉得这个问题可能成为系列中的第一个; - )
让我提供一些关于我到目前为止的背景,以便我的问题(希望)有意义。
现在,我有一个班级,MicroformatsParser
,完成所有工作。它有一个重载的构造函数,允许您传递包含URI的System.Uri
或string
:在构建时,它会在给定的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
属性(并且不是空白)class
属性与ContainerName
匹配的所有节点。将这些称为“容器节点”。T
类型的对象。MemberInfo[]
的公开字段(T
)
MemberInfo
PropertyName
属性
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)
等,但我想知道是否有更好地解决这个问题。
答案 0 :(得分:5)
Mehrdad的方法基本上是我建议的方法,但作为可能更多的第一步。
对于单个类型(字符串,基元等),您可以使用简单的IDictionary<Type,Delegate>
(其中每个条目实际上从T
到Func<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
上。
我不是说这是世界上最好的代码,但似乎工作正常。