使用属性和激活实现工厂模式






4.85/5 (32投票s)
2003年3月10日
6分钟阅读

94746

302
本文将向您展示如何使用属性和激活来实现工厂模式。
引言
工厂模式的基本规则是,工厂应创建具有已知祖先且实现隐藏的对象实例。具体创建哪个对象由工厂根据传递给工厂实例化对象的工厂方法的标准来决定。
我还想分享一些我通常在编写工厂时遵循的规则。
- 如果您需要工厂能够创建比最初设计时更多的类,您不应该修改工厂代码。
- 工厂应该与调用者一样不知道它所能实例化的类的实现。
- 工厂应该提供一种固定的机制来决定实例化哪个类。类本身应向工厂提供一些信息,工厂应以此为依据做出决定。
一种解决规则 1 和 2 中提出问题的方法是让工厂拥有一个内部类型列表,它可以实例化这些类型,并提供一个注册机制,以便可以告诉工厂它可以实例化哪些类。
为了解决第三条规则,需要让工厂可以实例化的类覆盖基类中的一个静态抽象方法,因为您需要在类实例化之前获取类的信息,并且需要覆盖派生类中的行为。这在 .NET 中是不可行的,因为 .NET 中没有静态抽象方法的概念,因此我们必须采用不同的方法:属性。
在本文中,我将向您展示如何实现一个使用属性来决定实例化哪个类的工厂。要完全理解本文,您需要对属性有一定的基本了解。
背景
为什么我认为这三条规则如此重要?
假设工厂是插件架构的一部分,并且由于它已编译到主机应用程序或单独的 DLL 中而无法修改。第三方应能轻松编写插件而无需修改工厂代码。即使当前情况暂时不需要这样做。如果您在开发系统时遵循这三条规则,那么当需求将来发生变化时,很容易实现这一点。
我还坚信,一个类应该是它自身的专家。既然一个类显然了解它自己的实现,为什么它不应该参与决定何时使用它的决策过程呢?如果类为工厂提供决策依据的信息,我们可以说该类是它自身的专家,因为它知道它做什么以及何时使用。
关于代码
为了说明如何构建这样的工厂,我决定使用以下案例。
任务是创建一个 ASP.NET 页面,当客户端请求该页面时,该页面会显示:“Hello World”。问题在于该页面应可供各种类型的客户端访问,例如 HTML 浏览器、XML Spy 和其他基于 XML 的工具、手机和其他 WML 浏览器。该页面也应该可以通过 telnet 客户端手动进行 GET 请求。
根据 User Agent,页面应以不同的方式生成,可以是 HTML、XML、WML 或纯文本(用于 telnet 客户端)。
为了简化演示,该应用程序实际上将是一个普通的 Windows 应用程序,其中包含一个用于输入“User Agent”的输入字段,以及一个用于显示输出的文本框。
解决此任务的基本思想是拥有一个抽象类
public abstract class ResponseFormatter
{
public abstract string FormatResponse(string Text);
}
它被多个类继承并在不同的方式下实现。例如,一个类被实现为使用 HTML 格式化文本,另一个类则使用 WML 等格式化文本。这些类是工厂负责实例化的类。
工厂类如下所示。
public sealed class ResponseFormatterFactory
{
private ArrayList RegisteredImplementations;
public ResponseFormatterFactory()
{
RegisteredImplementations = new ArrayList();
}
public void RegisterResponseFormatter(Type ResponseFormatterImpl)
{
if (!ResponseFormatterImpl.IsSubclassOf(typeof(ResponseFormatter)))
throw new ResponseFactoryError("Response Formatter
must inherit from class ResponseFormatter");
RegisteredImplementations.Add(ResponseFormatterImpl);
}
public ResponseFormatter CreateResponseFormatter(string UserAgent)
{
...
}
}
为了让工厂了解它能够实例化的类,它有一个 ArrayList
,其中包含已注册的每个 ResponseFormatter
实现的 System.Type
。因此,您必须使用 RegisterResponseFormatter
方法将所有实现都注册到工厂,才能让它了解它们。RegisterResponseFormatter
方法会检查 ResponseFormatter
参数实际上是否是派生自抽象类 ResponseFormatter
的类型。
工厂应管理的每个类都应“标记”一个或多个 UserAgentAttribute
,如下所示。
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class UserAgentAttribute : Attribute { private string UserAgentText; private bool ExactMatch; public UserAgentAttribute(string UserAgentText, bool ExactMatch) { this.UserAgentText = UserAgentText; this.ExactMatch = ExactMatch; } public virtual bool MatchesUserAgent(string UserAgent) { if (!ExactMatch) { return (UserAgent.ToLower().IndexOf(UserAgentText.ToLower()) != -1); } else return UserAgent.ToLower().Equals(UserAgentText.ToLower()); } }
AttributeUsage
属性表示此属性仅适用于类,并且可以在同一个类上使用多个此类属性。
在将此属性应用于类时,您必须提供两个参数:UserAgentText
和 ExactMatch
。第一个参数是要与您在调用工厂的 CreateResponseFormatter
方法时提供的用户代理进行测试的字符串。下一个参数定义了匹配的执行方式:精确匹配(true
)或部分匹配(false
)。
MatchesUserAgent
方法执行实际测试,并根据这两个参数决定该类是否可以服务指定的 UserAgent
。
我们来看一个实现抽象 ResponseFormatter
的类。
[UserAgent("Mozilla", false)]
public class HTMLResponseFormatter : ResponseFormatter
{
public HTMLResponseFormatter() {}
public override string FormatResponse(string Text)
{
System.Text.StringBuilder Response = new System.Text.StringBuilder();
Response.Append("<html></body>");
Response.Append(Text);
Response.Append("</body></html>");
return Response.ToString();
}
}
首先,我们有属性,它表明当 useragent 包含(部分匹配)字符串“Mozilla”时应使用此类。
FormatResponse
方法被重写,以将变量 Text
中的字符串格式化为 HTML。
现在,让我们查看 ResponseFormatterFactory
类中的 CreateResponseFormatter
方法。
public ResponseFormatter CreateResponseFormatter(string UserAgent)
{
// loop thru all registered implementations
foreach (Type impl in RegisteredImplementations)
{
// get attributes for this type
object[] attrlist = impl.GetCustomAttributes(true);
// loop thru all attributes for this class
foreach (object attr in attrlist)
{
if (attr is UserAgentAttribute)
{
// okay, we found an useragent attribute
// lets check if it matches our useragent
if (((UserAgentAttribute) attr).MatchesUserAgent(UserAgent))
{
// okay, this ResponseFormatter could be
// used with the specified UserAgent
// created instance and return
return (ResponseFormatter)
System.Activator.CreateInstance(impl);
}
}
}
}
// if we got this far, no ResponseFormatter implementation
// could be used with this UserAgent
throw new ResponseFactoryError("Could not find a ResponseFormatter
implementation for this UserAgent");
}
此方法包含两个嵌套循环:第一个循环遍历所有已注册的类型。第二个循环遍历当前类型的每个属性。如果属性是 UserAgentAttribute
,则调用该属性的 MatchesUserAgent
方法,以确定该类是否可以服务指定的 UserAgent
。如果 MatchesUserAgent
返回 true
,我们只需通过激活实例化该类型背后的类并返回它。如果循环在未找到要使用的实现的情况下完成,则会引发异常。
好吧,让我们把所有东西放在一起,看看工厂在哪里被使用。首先,我们将创建一个工厂实例并向其注册我们的实现。
// create factory
factory = new ResponseFormatterFactory();
// register implementations
factory.RegisterResponseFormatter(typeof(HTMLResponseFormatter));
factory.RegisterResponseFormatter(typeof(XMLResponseFormatter));
factory.RegisterResponseFormatter(typeof(TextResponseFormatter));
factory.RegisterResponseFormatter(typeof(WMLResponseFormatter));
接下来是最终使用工厂。
private void btnHello_Click(object sender, System.EventArgs e)
{
try
{
tbResponse.Text =
factory.CreateResponseFormatter(
cobxUserAgent.Text).FormatResponse("Hello World!!");
}
catch (ResponseFactoryError error)
{
MessageBox.Show(error.Message, "Error");
}
}
就是这样!
关注点
由于我是一名老练的 Delphi 程序员,我习惯了可以使用 static abstract
方法,或者在 Delphi 中称为:抽象 class function
。当我发现 .NET 中不可能实现这一点时,我沮丧了好几天,直到我偶然发现了属性!属性是程序员的强大工具,可以在某种程度上模仿抽象静态方法的行为,因为您也可以派生属性。
历史
- 2003-02-24 -- 初始发布
- 2003-02-28 -- 更新