65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 app.config 和反射实现的抽象工厂

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2006年8月7日

10分钟阅读

viewsIcon

42256

downloadIcon

239

一个实现链式模式的抽象工厂,通过 app.config 中的反射加载。

引言

抽象工厂是一种允许我们在运行时动态加载一组符合明确定义的接口的自定义功能的方法。为什么我们需要另一个工厂的示例?我提供了两篇来自 CodeProject 的优秀文章作为参考(见参考文献部分)!对于大多数工厂来说,在运行时构建所有可能的实现并让工厂选择最佳实现就足够了。然而,有时这还不够,并且要求工厂几乎不知道(或者很少知道)它可能需要哪些类。这包括那些可能提前定义好,但在每次运行时需要更改(app.config),或者在编译时完全不知道(不同的程序集和反射)的内容。在本文中,我将一起讨论这些内容以及链式模式,使示例更加真实。

app.config 文件

我认为,在这种情况下,在开始任务之前了解我们的目标很重要。我们的目标是创建一个 app.config 条目,用于通知我们的抽象工厂加载什么。由于我们还想展示链式模式,我们也必须将其纳入考虑。因此,我们有两个标准:定义 app.config 文件的一个节,告诉我们加载什么,以及 按什么顺序加载。

<section name="AbstractChainFactory" 
   type="AbstractFactory.FactorySectionHandler, AbstractChainFactory" />

这应该放在我们的 <configuration><configSections> 部分。它可能是你 app.config 文件中的前几行,并允许我们满足第一个标准。<section> 仅仅是通知设置解析器,我们期望这会导向另一个部分。name="AbstractChainFactory" 是节的实际名称。节是通过名称引用的,这允许你定义一个工厂,并让该工厂选择多个命名的节之一。type="AbstractFactory.FactorySectionHandler, AbstractChainFactory" 这是实际的难点。为了让 C# 自动加载我们的节,我们需要一个 System.Configuration.IConfigurationSectionHandler 来完成繁重的工作。type 属性告诉解析器在检查节时使用哪个 System.Configuration.IConfigurationSectionHandler 处理程序。type="" 实际上是 System.Type 需要解析的字符串,以便通过反射获取类。注意到字符串可以如此简单吗?如果你之前看过其中一些,它们会非常长而且奇怪。这是开始所需的最少内容,但我打算写另一篇文章介绍更多选项以及如何使用它们。

<AbstractChainFactory>
  <ChainClass type="AnotherAssembly.ChainClass1, AnotherAssembly" />
  <ChainClass type="AnotherAssembly.ChainClass2, AnotherAssembly" />
  <ChainClass type="AnotherAssembly.ChainClass3, AnotherAssembly" />
</AbstractChainFactory>

这应该放在我们的基本 <configuration> 部分,与 <configSections> 同级。我们在这里注意到,我们的节定义中的名称与此处的名称相同。这是故意的。只要名称匹配,它们可以是任何你想要的。查看内部,我们看到 <ChainClass type="AnotherAssembly.ChainClass1, AnotherAssembly" />。这就是我们开始做出实现决策的地方。我决定既然我们想要按顺序加载程序集,列表是合适的。因此,我将 XML 节点命名为 "ChainClass"。这纯粹是一个设计决策,只要它们在这里和我们的代码中的名称相同,我们就可以处理它。另外,请注意 type="" 的格式也相同吗?这是因为我们使用 System.Type.GetType 来进行反射,所以这应该不足为奇。同样,System.Type 允许类型字符串包含许多额外的参数。那是另一篇文章的内容。现在我们有了 app.config 外观的基础;测试代码中有更多示例,让我们继续。

快速查看引用

references screenshot

System.Configuration

为了使用反射和 app.config 文件,如我所述,我们需要在我们的引用列表中添加一个 DLL。System.Configuration.dll 包含 System.Configuration.ConfigurationManager(这是我们的第一行代码),并且必须添加。请注意它在 System.Configuration 命名空间中?System.Configuration 命名空间恰好存在于许多程序集中,但我们需要这个。看到 using System.Configuration; 但其中没有 ConfigurationManager,这可能令人头疼。

抽象工厂

注意到我们的演示项目包含 AbstractFactory 引用了吗?这是有道理的。由于我们的演示项目将使用 AbstractFactory.AbstractChainFactory,所以我们应该在这里看到它。另外请注意,AnotherAssemblie 项目包含对 AbstractFactory 的引用吗?这也是合理的,因为 AnotherAssemblie 中的类实现了 AbstractFactory.IChainable 接口。但是,没有任何对 AnotherAssemblie 的引用。这意味着当我们编译代码时,没有人能够访问它的类。这就是本次练习的重点。 即使在编译之后,我们也可以创建一个实现 AbstractFactory.IChainable 的程序集,并且我们可以在代码中运行它,而无需编译除了我们新程序集之外的任何东西。下一个问题是,如何让程序集被我们的程序看到?在开发环境中,这可能意味着一个安装包,但对我们来说,在 anotherassemblie 项目中添加构建后事件

copy "$(TargetPath)" "$(SolutionDir)$(OutDir)"

就足够了。纯粹主义者会注意到,这不一定是我们想要的,但对于大多数用途来说,这已经足够了。

post build event screenshot

代码概览

写文章时,总是在过于简单(为了使用需要更多研究)和过于复杂(通常需要研究最小功能以满足用户需求)之间走钢丝。这就是为什么我决定我的工厂将加载链式模式。链式模式易于实现和理解。它只是接收一个特定类型的对象,进行一些处理,然后返回相同类型以供链中的另一个环节重新处理。这样做是为了让我们能够看到抽象工厂中出现的一些错误,而不仅仅是一大堆需要加载的参数。

IChainable

一个简单的接口,定义了一个简单的链式模式。链式模式可以更复杂,但我们在这里是为了看工厂模式,而不是链式模式。

IChainedClass / ChainedClass

在 .NET 的一切事物中,微软提供了许多做事的方法。完全有可能使用反射动态加载一个类型为 Object 的对象,并使用反射调用该对象上的方法。我强烈建议只有在有意义时才这样做。我们这里有一个工厂,它应该产生一个明确定义的接口(或抽象类)的实例。这迫使编译器为我们进行各种类型检查和其他有用的工作。我见过返回对象的工厂,并使用反射来确定要对这些对象进行哪些调用;这并不好看。IChainedClass 是由工厂生成的明确定义的接口。ChainedClass 是该接口的实现,它知道链应该如何工作,并提供该功能。注意:大多数抽象工厂模式返回一个抽象类,而不是一个接口。对于链式模式,这是一个偏好的问题,因为链式模式通常很简单,因此您不需要大量具有共同功能被提取到基类中的独立类。纯粹是个人偏好。

FactorySectionHandler

这是另一种“许多方法/个人偏好”的情况。这里没有任何功能!我倾向于将所有用于动态反射加载的功能都放在一个地方,即 AbstractFactory.AbstractChainFactory,这样当出现问题时,它们都在同一个地方。如果你有几个工厂具有共同的功能,你可能会认真考虑在这里提取通用功能。在这一点上,就这个例子而言,没有必要这样做。

ChainClass1 / ChainClass2 / ChainClass3

这些只是为了表明我们确实在不同的程序集中加载了不同的类。并且它们都按照我们期望的顺序工作。

AbstractChainFactory

这是项目的真正核心。我们这里有一个重载的方法。

public static IChainedClass CreateChainedClasses()
public static IChainedClass CreateChainedClasses(string section name)

我们之所以这样做,是因为在我们的代码中,完全有可能多次调用工厂来生成相同的对象,但具有不同的参数(不同的链顺序)。还记得上面提到的,我们可以给我们的节起任何名字吗?这让我们能够拥有那些多个不同的对象

XmlNode node = (XmlNode)ConfigurationManager.GetSection(sectionname);

使我们能够拥有多个配置节。

if(node == null)
    throw new ConfigurationErrorsException("named section dose not exist");

在代码中存在一个在 app.config 文件中不存在的节是完全可能的。我见过这种情况,当 VS2005/VSS 决定用源代码管理中来自其他人(意外地)提交的包含他们测试环境的最新版本覆盖你的 app.config 文件时。另外,复制粘贴错误也占了很多因素。现在来谈谈架构。根据你的应用程序架构或需求,记录此错误并继续可能是可以接受的。现在,你可能需要立即返回 null,否则稍后会遇到 NullReferenceException。

List<IChainable> list = new List<IChainable>();

这只是链式模式的一部分。我们无法预知我们的链有多长,因此我们需要 System.Collections.Generic.List

Type t = Type.GetType(kid.Attributes["type"].Value); 
...
object o = t.Assembly.CreateInstance(t.FullName);
...
IChainable item = o as IChainable;

这代表了代码的实际处理部分,我选择在深入探讨架构和可接受错误之前先介绍它。我们在 app.config 中放入的字符串是 System.Type 动态加载程序集然后创建我们的类型的最小必需品。注意到 t.Assembly.CreateInstance(t.FullName) 了吗?我们隐式地使用了默认的无参数构造函数。如果我们的对象需要参数来构造,可能会有问题,但幸运的是,我们在这里不需要它,我将写另一篇文章,部分内容将涉及这个问题。IChainable item = o as IChainable; 与说 IChainable item = (IChainable)o; 大致相同,只是后者在无效转换时会抛出错误,而前者只会返回 null。我认为检查 null 比捕获异常看起来更整洁。个人选择。

if (!kid.Name.Equals("ChainClass"))
    throw new ConfigurationErrorsException("named section is ill defined");
if(kid.Attributes["type"] == null)
    throw new ConfigurationErrorsException("named section is ill defined");
if (kid.Attributes.Count != 1)
    throw new ConfigurationErrorsException("named section is ill defined"); 
...
if(t == null)
    throw new ConfigurationErrorsException("defined class dose not exist"); 
...
if(o == null)
    throw new ConfigurationErrorsException("defined class dose not exist"); 
...
if(item == null)
    throw new ConfigurationErrorsException("defined class" + 
                          " dose not implement IChainable");

是的。错误检查。和以前一样,所有这些错误都取决于架构。在架构层面,人们会决定哪些错误是可以接受的,哪些是可以恢复的。我们在这里关注的是后者。这里的每个错误都试图检查链式模式或 app.config 的常见问题。有人声称我们应该使用 SAX 风格的方法来获取我们能获取的一切,并忽略我们无法处理的内容。我通常同意,除了对象构造的情况。对象是面向对象设计的基石。如果我们不能信任我们的对象至少是按照我们预期的方式创建的,我们将陷入困境。我曾经参与过一个项目,最初决定在出现错误时加载不完整的链是可以接受的。这看起来似乎合理,除了一个拼写错误导致我浪费了很多时间,我一直在想为什么我的处理没有全部完成。我的所有对象都在那里。只是链中的一个环节没有加载,而链非常依赖顺序。所以在我看来,如果我们的链(或更普遍地说,工厂对象)没有全部加载,返回 null 可能是可以接受的,但允许部分构造可能是一个非常糟糕的主意。

演示应用程序

Sample screenshot

演示应用程序没什么特别的。顶部的五个按钮可以让你尝试动态加载一个有效的链(使用默认方法)、一个有效的链(使用备用方法)或三个由于 app.config 文件中的常见错误而导致的无效加载。

参考文献

© . All rights reserved.