C# 中类工厂的实现






4.85/5 (31投票s)
2003年1月7日
9分钟阅读

133421

147
详细介绍了在 C# 中实现类工厂的五种方法。
目录
- 引言
- 背景
- 测试程序
- 使用 System.Activator.CreateInstance
- 使用哈希表和 Switch 语句
- 使用带名称比较的 If 语句
- 使用带字符串的 Switch 语句
- 使用元素工厂
- 性能测量
- 结论
- 历史
引言
有几篇文章描述了如何在 C# 中编写模式。然而,示例实现通常性质简单。由于之前使用类工厂的经验,我对 C# 中类工厂的实现产生了兴趣,但对目前看到的工作并不满意。本文提供了五种示例实现并比较了它们的性能。
背景
在我之前作为 Delphi 程序员的工作中,我编写了一个 XSLT 处理器,可以将 XSLT 样式表应用于 XML 文档。其中一项任务是实现一个类工厂,根据刚从样式表中读取的 XSLT 元素实例化正确的类。例如,如果 XSLT 处理器在样式表中遇到 xsl:value-of
元素,它将创建 TXslValueOf
类的一个实例来表示该元素。
在 Delphi 中,这是一项微不足道的任务,因为类就是对象。将类的引用存储在哈希表中,以 XSLT 元素的名称作为键,这很简单。当遇到元素名称时,可以从哈希表中检索类并调用其 Create
方法。
我开始对在 C# 中找到类似优雅的解决方案感兴趣。一位同事指引我使用 System.Activator
。在尝试了一段时间后,我研究了另外两种实现,看看是否能获得更好的性能。本文的第一个版本促使另外两个人,Wesner Moise 和 M. Veurman,提出了修改和替代实现。
测试程序
测试程序是一个控制台程序,由五部分组成:一个 Director
类、一个 Tester
类、一个定义静态数据的 Globals
类、许多表示 XSLT 元素的类以及几个类工厂实现。Director
类实例化 Tester
,告诉 Tester
运行性能测试,然后报告每个性能测试的平均时间。Director
代码的一部分如下:
[STAThread]
static void Main(string[] args)
{
Tester tester = new Tester(instances);
Console.WriteLine(String.Format("Running {0} passes of each test, " +
"creating {1} instances per test",
passes, instances));
Console.WriteLine(Environment.NewLine);
// Run hash-switch test.
Console.WriteLine("Running Hash-Switch test...");
int Total = 0;
for (int i = 0; i < passes; i++)
Total += tester.CreateViaHashSwitch();
// Report the average.
Console.WriteLine("Hash-Switch: " + (Total/passes) + " ms");
Console.WriteLine(Environment.NewLine);
...
}
Globals
类定义了一个 XSLT 元素名称的静态数组。此数据由某些类工厂实现使用。
// Defines globally-available, static data.
public class Globals
{
// XSLT element names
public static string[] ElementNames = new string[24]
{
"xsl:apply-imports",
"xsl:apply-templates",
...
};
}
表示 XSLT 元素的类不做任何事情。它们存在的唯一目的是为 Tester
提供可实例化的东西。XSLT 元素类都继承自 BaseElement
类,并具有诸如 XslApplyTemplates
和 XslForEach
等名称。
Tester
类实现了几个测试方法。例如,CreateViaActivator
、CreateViaHashSwitch
和 CreateViaIfName
。每个测试方法都会实例化适当的类工厂,并测量工厂创建指定数量对象所需的时间。
每个类工厂实现都位于其自己的文件中(例如,ActivatorFactory
类位于文件 ActivatorFactory.cs 中)。这个测试程序声明了一个抽象类工厂,带有一个抽象的 Create
方法,用于实例化所需类型的对象。每个具体类工厂都实现了 Create
方法。实际实现将在以下部分中描述。
使用 System.Activator.CreateInstance
我的第一个类工厂实现使用了 System.Activator
的 CreateInstance
方法。该实现位于文件 ActivatorFactory.cs 中的 ActivatorFactory
类中。
System.Activator.CreateInstance
方法有多个重载,我的第一次尝试使用了期望程序集名称和类名称的重载。然而,我发现我通过反复将名称转换为类型来浪费时间。我的最终实现使用哈希表将 XSLT 元素的名称映射到类类型,然后将类类型传递给 CreateInstance
。如以下代码片段所示,哈希表在 ActivatorFactory
构造函数中初始化:
public class ActivatorFactory : BaseClassFactory
{
// The following hash table maps an XSLT element name to a
// System.Type.
private Hashtable _ElementHash;
public ActivatorFactory()
{
// Populate the hash table.
_ElementHash = new Hashtable(50, (float)0.5);
_ElementHash.Add(Globals.ElementNames[0], typeof(XslApplyImports));
...
}
...
请注意,Hashtable
是以小于 1.0 的加载因子构造的。正如 Wesner Moise 指出的那样,这显著减少了查找键所需的时间。类工厂的 Create
方法(即实例化正确类的对象的方法)的实现如下:
public override BaseElement Create(string name)
{
System.Type type = (System.Type) _ElementHash[name];
return (BaseElement) System.Activator.CreateInstance(type);
}
对我来说,使用 System.Activator
是优雅的。如果添加更多类,唯一需要做的更改是更新类名称的常量数组并在工厂的构造函数中向哈希表添加一个条目。然而,我曾被警告这种方法可能很慢。为了验证警告是否有效,我实现了其他类工厂(参见以下部分)。事实证明,使用 System.Activator.CreateInstance
可能比本文中介绍的替代方案慢几倍。
使用哈希表和 Switch 语句
类工厂的下一个实现再次使用了哈希表。它不是将元素名称映射到类类型,而是将名称映射到整数索引。然后,switch
语句使用该索引实例化正确的类。该实现位于文件 HashSwitchFactory.cs 中的 HashSwitchFactory
类中。
以下代码片段显示了哈希表初始化的一部分:
public class HashSwitchFactory : BaseClassFactory
{
// The following hash table maps an XSLT element name to an
// integer index. The Create method uses the index to identify
// the class to be instantiated.
private Hashtable _ElementHash;
public HashSwitchFactory()
{
// Populate the hash table.
_ElementHash = new Hashtable(50, (float)0.5);
_ElementHash.Add(Globals.ElementNames[0], 0);
_ElementHash.Add(Globals.ElementNames[1], 1);
...
}
...
以下代码片段显示了 Create
方法中 switch
语句的一部分:
public override BaseElement Create(string name)
{
switch ((int) _ElementHash[name])
{
case 0:
return new XslApplyImports();
case 1:
return new XslApplyTemplates();
...
}
}
这种类工厂方法比使用 System.Activator.CreateInstance
快几倍。然而,它需要更多的源代码来实现(我不喜欢大的 case 语句),并且在我看来可维护性较低。
使用带有名称比较的 If 语句
当我查看 Rotor 源代码以了解 Microsoft 如何根据元素名称实例化 XSLT 类时,我发现他们使用了大量的 if..else
语句。每个子句都将元素名称与记录中的一个名称进行比较。如果匹配,则实例化关联的类。为了完整起见,我在文件 IfNameFactory.cs 中添加了 IfNameFactory
类,以计时使用相同方法的实现。工厂的实现如下:
public class IfNameFactory : BaseClassFactory
{
public override BaseElement Create(string name)
{
if (name == Globals.ElementNames[0])
{
return new XslApplyImports();
}
else if (name == Globals.ElementNames[1])
{
return new XslApplyTemplates();
}
else if (name == Globals.ElementNames[2])
...
}
}
如您所见,代码看起来与使用哈希表和 switch
语句的情况非常相似。然而,使用 if
语句的性能因需要进行的比较次数而异。测试程序的最终版本实例化了 24 个不同的类。对于 24 个类,if
语句比 switch
语句慢。当类数量减少时(例如,10、15),if
语句比 switch
语句快。对于这个特定的测试程序,性能提升在大约 17 或 18 个类时下降了。
在这种特定的实现中,每个类实例化的次数与其他类相同。如果某些类实例化的次数多于其他类,则可以将这些类的测试放在 if
语句的更早位置,从而提高性能。
您可以通过 Tester
类中的 cutoff
常量指定要实例化的类数量。
使用带字符串的 Switch 语句
本文首次发布后不久,Wesner Moise 指出 switch
语句可以与字符串一起使用。该实现定义为文件 SwitchNameFactory.cs 中的 SwitchNameFactory
类,如下所示:
public class SwitchNameFactory : BaseClassFactory
{
public override BaseElement Create(string name)
{
switch (name)
{
case "xsl:apply-imports":
return new XslApplyImports();
case "xsl:apply-templates":
return new XslApplyTemplates();
...
}
}
与 IfNameFactory
一样,它不需要哈希表。所有逻辑和维护都包含在 switch
语句中。在性能方面,随着类数量的增加,它可能比 HashSwitchFactory
慢,但比 IfNameFactory
快。
使用元素工厂
M. Veurman 提出了另一种注重面向对象的实现。该实现包含在单元 ElementFactory.cs 中,定义了一个表示实例化 XSLT 元素的类工厂接口。然后它为每个 XSLT 元素定义一个具体的类工厂。接口和示例类如下代码片段所示:
// Interface for the element factory.
public interface IXslElementFactory
{
string ElementName
{
get;
}
BaseElement Create();
}
// Concrete classes for element factories.
public class XslApplyImportsFactory : IXslElementFactory
{
public string ElementName
{
get { return "xsl:apply-imports"; }
}
public BaseElement Create()
{
return new XslApplyImports();
}
}
...
名为 ElementFactory
的类工厂在其构造函数中注册每个元素工厂。该注册将元素工厂的元素名称映射到元素工厂的实例。
public class ElementFactory : BaseClassFactory
{
Hashtable _ElementHash;
public ElementFactory()
{
// Create the hash table.
_ElementHash = new Hashtable(50, (float)0.5);
// Register the element class factories.
RegisterElementFactory(new XslApplyImportsFactory());
RegisterElementFactory(new XslApplyTemplatesFactory());
...
}
...
private void RegisterElementFactory(IXslElementFactory factory)
{
_ElementHash.Add(factory.ElementName, factory);
}
ElementFactory.Create
方法从哈希表中检索元素工厂并调用元素工厂的 Create
方法。
public override BaseElement Create(string name)
{
IXslElementFactory factory = (IXslElementFactory) _ElementHash[name];
return factory.Create();
}
这种实现无疑是面向对象的。在性能方面,它表现得非常好。用这个测试程序测量时,当类工厂生成 18 个或更多类时,它位列第二。然而,在这种情况下,面向对象是有代价的。每个类工厂支持的类需要更多的代码行。这可能是好是坏,取决于个人和项目。
性能测量
测试程序的发布版本在一台配备 2.4 GHz Pentium 4 CPU 和 512 MB RAM 的计算机上执行。每个测试执行 10 次,每次实例化一百万个对象。报告的时间是 10 次执行的平均值。工厂列按实例化 24 个类从最快到最慢排序。
实例化类 | 哈希开关 | 元素工厂 | 开关名称 | If-名称 | System.Activator |
5 | 273 毫秒 | 279 毫秒 | 296 毫秒 | 98 毫秒 | 1303 毫秒 |
10 | 267 毫秒 | 275 毫秒 | 287 毫秒 | 171 毫秒 | 1298 毫秒 |
15 | 264 毫秒 | 275 毫秒 | 286 毫秒 | 237 毫秒 | 1303 毫秒 |
18 | 265 毫秒 | 276 毫秒 | 287 毫秒 | 281 毫秒 | 1300 毫秒 |
20 | 268 毫秒 | 282 毫秒 | 290 毫秒 | 312 毫秒 | 1300 毫秒 |
24 | 273 毫秒 | 289 毫秒 | 293 毫秒 | 362 毫秒 | 1309 毫秒 |
结论
每种类工厂实现都具有不同程度的性能和灵活性。这个测试程序必须创建一百万个对象实例才能显示出明显的性能差异。许多实际项目是否需要在短时间内创建这么多对象?可能不会。因此,对于大多数情况,在决定使用哪种实现时,可维护性可能比性能更重要。
在可维护性方面,这些实现可以按从所需维护最少到所需维护最多进行排名。请注意,前两组工厂之间的实际差异仅为几行代码。
SwitchNameFactory
,IfNameFactory
HashSwitchFactory
,ActivatorFactory
ElementFactory
如果面向对象是优先事项,那么 ElementFactory
是这批中最面向对象的,并且具有非常好的性能。
如果类工厂用于创建少量对象且创建这些对象的时间不重要的情况,使用 ActivatorFactory
可以提高代码的可维护性和优雅性。
如果类工厂将创建大量对象,并且可实例化的类数量相对较少(例如,20 个或更少),则使用 IfNameFactory
可以获得最佳性能。通过在 if
语句的开头测试频繁实例化的类,可以提高性能。
如果类工厂将创建大量对象,并且可能实例化二十个或更多类,则可以通过使用 HashSwitch
、ElementFactory
或 SwitchNameFactory
来获得最佳性能。
历史
- 2003 年 1 月 7 日 - 初次发布
- 2003 年 1 月 17 日 - 重构代码以展示实际的类工厂实现。添加了元素工厂和按名称开关的实现。