第2篇 创建型模式
管理并隔离对象实例的构造过程
创建型模式是为了隔离客户程序与具体类型实例化的依赖关系,通过将实例化职责委托他方对象的办法,保证客户程序(或外部系统)获得期望具体类型实例的同时不必发生直接的引用。
此篇介绍GOF23中各种创建型模式的工程化实现。
第3章工厂&工厂方法模式
3.1 简单工厂
3.2 经典回顾
3.3 解耦Concrete Factory与客户程序
3.4 基于配置文件的Factory
3.5 批量工厂
3.6 基于类型参数的Generic Factory
3.7 委托工厂类型
3.8 小结
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses..
— Design Patterns : Elements of Reusable Object-Oriented Software
笔者一直认为工厂方法是整个创建型模式中最为典型的、也是最具启发效果的,它告诉我们使用一个变化频率比较高的类不必忙着new(),而要依赖一个抽象的类型(抽象类或接口)。根据准备知识的介绍,.NET平台上Delegate也是一个抽象,与抽象类型不同,它是对一类方法的抽象,而不像前两者是对一组方法的抽象。哪一种更好呢?
尺有所长,寸有所短。
如果需要的仅仅是某个特定的操作,那么大可不必按照抽象类型来加工,反馈一个Delegate实例就可以了;如果需要的是抽象业务实体,或者是具有一组“操作+属性”的抽象类型,那么就循规蹈矩好了。
使用工厂方法的主要动机来自于“变化”,应用的哪些组成会快速变化呢?不一定,多数项目会有一个相对稳定的核心,无论是被叫为Framework,还是现在更时髦的、感觉上更底层的Foundation,这个部分相对比较稳定,言外之意其他部分都会“相对”变化比较频繁。
我们可能期待架构师设计一个很灵活的架构,这样开发的时候就可以“填空”,而且是Plug && Play方式的;我们也可以期待需求分析人员把需求分析得特别透彻,这样我们代码真的Write Once Run Always了。当然,既然是Team工作,就有上下家,周围还有共同奋斗的同事们,我们希望上家的变化尽可能地少,同时出于人际工程学的需要,我们也尽量不为下家找麻烦。
很难。
基于接口的开发虽然不能解决上述那么多问题,但起码可以从很多方面减轻这些工作负担。如果想贯彻这种思想,那么首先要从类型构造上解决问题,否则后面的讨论很容易都成为空谈。(当然,可以通过正交的方法,或者一般被称为“拦截”的方式在具体方法执行上提供灵活性。后面章节会对此进行描述。)
3.1 简单工厂
3.1.1 最简单的工厂类
作为Factory Method、Abstract Factory的一个“预备”,首先用最朴实的方式完成一个工厂(其结构如图3-1所示),体现工厂与抽象类型间的构造关系,其代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.FactoryMethod.Example1 { public interface IProduct { } public class ConcreteProductA : IProduct { } public class ConcreteProductB : IProduct { } public class Factory { public IProduct Create() { //工厂决定到底实例化哪个子类 return new ConcreteProductA(); } } }
Unit Test
参考FactoryMethod.Test项目Example1的FactoryTest /// <summary> /// 说明可以按照要求生成抽象类型,但具体实例化哪个类型由工厂决定 /// </summary> [TestMethod] public void Test() { Factory factory=new Factory(); IProduct product=factory.Create(); Assert.AreEqual<Type>(product.GetType(), typeof(ConcreteProductA)); }
图3-1 简单工厂的静态结构
这个工厂与直接使用new()有什么不同?假如把它放到工程中应用有什么不尽如人意的地方?
● 通过IProduct隔离了客户程序与具体ConcreteProductX的依赖关系,在客户程序视野内根本就没有ConcreteProductX。
● 即使ConcreteProductX增加、删除方法或属性,也无妨大局,只要按照要求实现了IProduct就可以,Client无须关心ConcreteProductX的变化(确切地说它也关心不着,因为看不到)。
● 相对直接写个ConcreteProductX而言,要平白地多写一个工厂出来,尤其当需要IProduct频繁变化的时候,客户程序也闲不下来。
● 好的需求分析师可以在实施之前分析清楚85%的需求,好的架构师在把这些需求转换为实际技术框架的时候大概能做到90%的忠于需求,作为开发人员,设计的时候能够详细刻画95%的内容就很不错了,这样100% - 85% * 90% * 95%=27.3%,也就是说,即便您身在一个精英团队,也可能有1/4的内容到了编码的时候还无法得到准确分析,那么对应用的某个局部领域而言,很难有效抽象这个IProduct;但Factory提示在第一遍Draft的时候,我们就可以直接new(),但在复查或迭代的过程中一定要尽量找到IProduct,然后套个Factory,在公共代码部分,更是如此。
此外,还有个效率问题。如果构造一个Factory实例,并用它获取一个抽象类型实例后就不再使用,资源上有些浪费,尤其当这个过程非常频繁的时候。工程中可以通过如下几种方式解决:
● 把工厂实例作为参数注入到操作中,而不是在每个方法内部自行创建,其代码如下:
C# Enterprise Library Data Access Application Block Database.cs片断节选
private DbProviderFactory dbProviderFactory; protected Database(string connectionString, DbProviderFactory dbProviderFactory) { this.dbProviderFactory=dbProviderFactory; } public virtual DbConnection CreateConnection() { DbConnection newConnection=dbProviderFactory.CreateConnection(); newConnection.ConnectionString=ConnectionString; return newConnection; }
● 把工厂设计为Singleton方式,因为工厂的职责相对单一,所有客户端需要的加工过程使用的都是一个唯一的共享实例。
● 使用静态类。虽然在经典的设计模式书籍示例中并没有采用,但它未尝不是一个有效节省资源使用的途径,不过切记:静态类不能被继承它只能从object继承,没其他可能。所以看起来静态类更像以前的API集合,有些不那么“面向对象”的味道。其示例代码如下:
C#
using System; namespace MarvellousWorks.PracticalPattern.FactoryMethod.Example1 { public enum Category { A, B } public static class ProductFactory { public static IProduct Create(Category category) { switch (category) { case Category.A: return new ConcreteProductA(); case Category.B: return new ConcreteProductB(); default: throw new NotSupportedException(); } } } }
Unit Test
/// <summary> /// 说明静态工厂可以根据提供的目标类型枚举变量选择需要实例化的类型 /// </summary> [TestMethod] public void Test() { IProduct product=ProductFactory.Create(Category.B); Assert.IsNotNull(product); Assert.AreEqual<Type>(typeof(ConcreteProductB), product.GetType()); }
静态工厂的静态结构如图3-2所示。
图3-2 静态工厂的静态结构
3.1.2 根据规格加工产品——参数化工厂
本节中最初那个程序(IProduct、Factory)只能是个Example,充其量也就是个模型。为什么?我们有两个ConcreteProductX,如果没有任何机制告诉Factory该构造谁,工厂只能采用上面那个“认A为亲”,或者随机产生A/B之中某一个实例的方法,这实在不靠谱。如果想让工厂在运行态有效选择符合抽象类型要求的某个实例,最简单的机制莫过于传递一个参数,它可能是一个字符串、一个HRESULT值、一个枚举值,或者是类似Enterprise Library中的某个IConfigurationSource……,例如在上例静态类工厂的实现中就显式地传递了一个Category类型的枚举参数。
3.1.3 简单工厂的局限性
上例的简单工厂比较优雅地解决了外部new()的问题,它把目标实例的创建工作交给一个外部的工厂完成,是设计模式化思想一个很不错的引子。但如果应用中需要工厂的类型只有一个,而且工厂的职责又非常单纯——就是一个new()的替代品,类似我们面向对象中最普遍的思路,这时候就需要进一步抽象了,于是出现了新的发展:工厂方法模式和抽象工厂模式。
3.2 经典回顾
作为简单工厂的一个扩展,工厂方法的意图也非常明确,它把类的实例化过程延迟到子类,将new()的工作交给工厂完成。同时,增加一个抽象的工厂定义,解决一系列具有统一通用工厂方法的实体工厂问题。它适合如下情景:
● 客户程序需要隔离它与需要创建的具体类型间的耦合关系。
● 很多情况下,客户程序在开发过程中还无法预知生产环境中实际要提供给客户程序创建的具体类型。
● 将创建性工作隔离在客户程序之外,客户程序仅需要执行自己的业务逻辑,把这部分职责交给外部对象完成。
● 如果目标对象虽然可以统一抽象为某个抽象类型,但它们的继承关系太过复杂,层次性比较复杂,这时同样可以通过工厂方法解决这个问题。
工厂方法主要有四个角色。
●抽象产品类型(Product):工厂要加工的对象所具有的抽象特征实体。
● 具体产品类型(Concrete Product):实现客户程序所需要的抽象特质的类型,它就是工厂需要延迟实例的备选对象。
● 声明的抽象工厂类型(Creator):定义一个工厂方法的默认实现,它返回抽象产品类型Product。
● 重新定义创建过程的具体工厂类型(Concrete Creator):返回具体产品类型 Concrete Product的具体工厂。
于是,经典的工厂方法设计结构如图3-3所示。
图3-3 经典工厂方法模式的静态结构
示例代码如下:
C#抽象产品类型与具体产品类型
///抽象产品类型 public interface IProduct { string Name { get;} // 约定的抽象产品所必须具有的特征 } /// 具体产品类型 public class ProductA : IProduct { public string Name { get { return "A"; } } } public class ProductB : IProduct { public string Name { get { return "B"; } } }
C#抽象的工厂类型描述
public interface IFactory { IProduct Create(); // 每个工厂所需要具有的工厂方法——创建产品 }
C#两个实体的工厂类型
public class FactoryA : IFactory { public IProduct Create() { return new ProductA(); } } public class FactoryB : IFactory { public IProduct Create() { return new ProductB(); } }
通过这个例子我们可以看到,当客户程序作为IProduct消费者的时候,抽象类型在虚拟构造(Virtual Constructor)的时候到底“挂”哪个实体类型由Factory决定。客户程序需要使用IProduct的时候通过访问IFactory所定义的Create()方法就可以获得一个目标产品实例,直观上客户程序可以把频繁变化的某个Product隔离在自己的视线之外,自身不会受到它的影响。
3.3 解耦Concrete Factory与客户程序
虽然经典工厂方法模式告诉了我们最后的结果,通过Product、Concrete Product、Factory和Concrete Factory 四个角色解决了客户程序对 Product的获取问题,但没有把客户程序和Factory连起来,也就是说,没有介绍如何把Factory放到客户程序里。示例代码如下:
C#两个实体的工厂类型
class Client { public void SomeMethod() { IFactory factory=new FactoryA(); // 获得了抽象Factory的同时与FactoryA产生依赖 IProduct product=factory.Create(); // 后续操作仅依赖抽象的IFactory和IProduct; // ...... } }
如果让客户程序直接来new()某个Concrete Factory,由于 Concrete Factory依赖于Concrete Product,因此还是形成了间接的实体对象依赖关系,背离了这个模式的初始意图。怎么办?在前面的介绍中笔者提到了“依赖注入”的概念,也就是通过Assembler把客户程序需要的某个IFactory传递给它。因此,静态实现结构要在经典工厂方法模式上做些修改,如图3-4所示。
图3-4 增加了装配对象的工厂方法静态结构
实际工程中很多时候不得不做这项工作——将Concrete Product与客户程序分离,其原因是我们不想让上游开发人员的针头线脑的修改迫使我们必须重新Check Out代码,编译、重新单元测试后再Check In,这种活动在项目开发的中、后期常常是非常令人厌恶的,所以我们把这些“烂摊子”统统甩给Assembler。参考第1章“实现依赖注入”部分的介绍,我们给那个版本的Assembler多增加一个注册项,然后按照适合的注入实现方式把IFactory传给客户程序,其代码如下:
C#为Assembler增加有关IFactory的注册项
static Assembler() { // 注册抽象类型需要使用的实体类型 //实际的配置信息可以从外层机制获得,例如通过配置定义 dictionary.Add(typeof(ITimeProvider), typeof(SystemTimeProvider)); dictionary.Add(typeof(IFactory), typeof(FactoryA)); }
C# 相应客户程序就可以修改为依赖Assembler的方式
class Client { private IFactory factory; public Client(IFactory factory) // 外部机制将IFactory借助Assembler以Setter方式注入 { if (factory== null) throw new ArgumentNullException("factory"); this. factory= factory; } public void AnotherMethod() { IProduct product=factory.Create(); // ... ... } }
3.4 基于配置文件的Factory
上面的方法虽然已经解决了很多问题,但工程中我们往往会要求Assembler知道更多的IFactory/Concrete Factory配置关系,而且考虑到应用的Plug && Play要求,新写的Concrete Factory类型最好保存在一个额外的程序集里面,不涉及既有的已部署好的应用。为了不用反复引用并编译代码,而且可以把这些涉及运行维护的事务性工作交到系统管理员手里,一个常见的选择就是把Assembler需要加载的IFactory/Concrete Factory列表保存到配置文件里,相应的配置文件设计如下:
App.Config
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name ="marvellousWorks.practicalPattern.factoryMethod. customFactories" type= "System.Configuration.NameValueSectionHandler"/> </configSections> <marvellousWorks.practicalPattern.factoryMethod.customFactories> <add key="MarvellousWorks.PracticalPattern.FactoryMethod.Classic.IFactory, MarvellousWorks.PracticalPattern.FactoryMethod" value="MarvellousWorks.PracticalPattern.FactoryMethod.Classic. FactoryA, MarvellousWorks.PracticalPattern.FactoryMethod"/> </marvellousWorks.practicalPattern.factoryMethod.customFactories> </configuration>
由于这里IFactory/Concrete Factory仅仅是个映射关系列表,因此为了实现简介并不采用第1章配置部分介绍的自定义配置节的办法,而是直接使用.NET Framework自带的System.Configuration. DictionarySectionHandler实现。
Assembler则将提取IFactory/Concrete Factory的过程修改为通过配置实现,其代码如下:
C# 通过配置文件实现的Assembler
using System; using System.Collections.Generic; using System.Configuration; using System.Collections.Specialized; namespace MarvellousWorks.PracticalPattern.FactoryMethod.Classic { public class Assembler { ///配置节名称 private const string SectionName = "marvellousWorks.practicalPattern.factoryMethod.customFactories"; /// IFactory在配置文件中的键值 private const string FactoryTypeName="IFactory"; /// 保存“抽象类型/实体类型”对应关系的字典 private static Dictionary<Type, Type> dictionary = new Dictionary<Type, Type>(); static Assembler() { // 通过配置文件加载相关“抽象产品类型”/“实体产品类型”的映射关系 NameValueCollection collection=(NameValueCollection) ConfigurationSettings.GetConfig(SectionName); for (int i=0; i < collection.Count; i++) { string target=collection.GetKey(i); string source=collection[i]; dictionary.Add(Type.GetType(target), Type.GetType(source)); } } /// … … }
Unit Test
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using MarvellousWorks.PracticalPattern.FactoryMethod; using MarvellousWorks.PracticalPattern.FactoryMethod.Classic; namespace FactoryMethod.Test.Classic { public class Client { private IFactory factory; public Client(IFactory factory) // 将IFactory通过Constructor方式注入 { if (factory == null) throw new ArgumentNullException("factory"); this.factory=factory; } public IProduct GetProduct() { return factory.Create(); } } [TestClass] class TestClient { [TestMethod] public void Test() { IFactory factory=(new Assembler()).Create<IFactory>(); Client client=new Client(factory); // 注入IFactory IProduct product=client.GetProduct(); // 通过IFactory获取IProduct Assert.AreEqual<string>("A", product.Name); //配置中选择为FactoryA } } }
这样,即便在系统上线后,如果需要修改IFactory的具体类型,一样可以通过增加新的程序集,在生产环境中更新相关IFactory需要使用的具体工厂类型。这种方式下,客户程序成为一个可以动态加载的框架,外部机制很容易通过配置将新完成的程序集部署到运行环境中,而在经典的设计模式实现中,如果需要把模式工程化,很多时候需要借助外部的配置机制。
3.5 批量工厂
3.5.1 开发情景
实际项目中经常需要加工一批对象,这时候如果按部就班地采用单件生产模式来生成,效率上相对较低,最好专门设计独立的批量工厂。
其实,“工厂”这个名称体现的也是这个思想,现实中除了造船、造卫星、造航空母舰之类的工厂是单件生产外,大多数工厂都是一批批生产产品的,很难想象调用“啤酒Factory”的Create()方法的时候,要经过工厂的一系列处理,最后才产生一瓶啤酒,如果有人蹬个自行车准备批发一件啤酒的时候,要等24次序列处理,这也太累了吧。那我们看看如果准备批量生产产品,大体上需要哪些步骤?其步骤如图3-5所示。
图3-5 一个批量生产的处理工序示意图
实际项目中的开发过程也与此类似,但需要新增两个步骤。
● 准备生产:之前的实现中,实际加工对象的步骤其实就一个——new(),但之前必须编写很多访问配置文件,寻找抽象产品类型需要实例化的产品类型,项目中经常还需要增加很多其他例如记录日志、权限检查等之类的操作,这些步骤都被划分到“准备生产”过程里。
●配货:这是个可选步骤。在此以实际生产过程做类比,比如我们生产5000部手机,但这5000部手机并不一定都是黑色的,照顾到女性顾客的需要,可能其中要包括2000部红色的,因此后续实际生产不是for(int i=0; i<5000; i++)的单纯new(),还需要增加一个配货步骤。
参考经典设计模式其他模式的实现,依据单一职责原则,需要增加三个对象。
● 指导生产的抽象类型(Director):它告诉客户程序在保持IFactory的条件下,生产某类产品的数量,同时由于具体某类产品是由Concrete Factory决定的,因此客户程序实际获得IFactory还需要由Director告诉Assembler并进行替换。例如上面那个手机的情景,就需要Director在用完当前BlackMobileFactory生产3000部黑色手机后,将Client的IFactory换成RedMobileFactory,继续生产2000部红色手机。
● Director的每个步骤称为一个决策(Decision):它包括两个信息,例如当决策生产2000部红色手机的时候,它包括数量(2000)和实际加工对象(RedMobileFactory)两项,客户程序Decision记载的批次数量2000调用RedMobileFactory的Create()方法。
●为批量的IProduct增加一个集合容器类型,项目中也可以直接使用.NET Framework提供的既有集合类型(或泛型集合类型),同时修改Concrete Factory的加工方式,变返回某个单独的IProduct为返回IProduct集合。
另外,参考前面的设计,由于项目中批量的加工任务不一定只有一类产品,因此把Director这个“军师”放到客户程序中的任务,还是交给Assembly完成好了。下面是其实现步骤。
3.5.2 定义产品类型容器
产品容器的对象结构如图3-6所示。
图3-6 产品容器对象结构
示例代码如下:
C# 装载IProduct的容器类型
public class ProductCollection { private IList<IProduct> data=new List<IProduct>(); ///对外的集合操作方法 public void Insert(IProduct item) { data.Add(item); } public void Insert(IProduct[] items) // 批量添加 { if ((items == null) || (items.Length == 0)) return; foreach (IProduct item in items) data.Add(item); } public void Remove(IProduct item) { data.Remove(item); } public void Clear() { data.Clear(); } /// 获取所有IProduct内容的属性 public IProduct[] Data { get { if ((data == null) || (data.Count == 0)) return null; IProduct[] result=new IProduct[data.Count]; data.CopyTo(result, 0); return result; } } /// 当前集合内的元素数量 public int Count { get { return data.Count; } } ///为了便于操作,重载的运算符 public static ProductCollection operator +( ProductCollection collection, IProduct[] items) { ProductCollection result=new ProductCollection(); if (!((collection == null)||(collection.Count == 0)))result.Insert(collection.Data); if (!((items == null) || (items.Length == 0))) result.Insert(items); return result; } public static ProductCollection operator +( ProductCollection source, ProductCollection target) { ProductCollection result=new ProductCollection(); if (!((source == null) || (source.Count == 0))) result.Insert(source.Data); if (!((target == null) || (target.Count == 0))) result.Insert(target.Data); return result; } }
3.5.3 定义批量工厂和产品类型容器
在经典工厂方法模式上增加一个产品容器,如图3-7所示。
图3-7 增加了产品容器的批量工厂对象结构
示例代码如下:
C# 定义并实现批量产品加工工厂
using System; using MarvellousWorks.PracticalPattern.FactoryMethod; namespace MarvellousWorks.PracticalPattern.FactoryMethod.Batch { public interface IBatchFactory { /// <param name="quantity">待加工的产品数量</param> ProductCollection Create(int quantity); } ///为了方便扩展提供的抽象基类 /// <typeparam name="T">Concrete Product类型</typeparam> public class BatchProductFactoryBase<T> : IBatchFactory where T : IProduct, new() { public virtual ProductCollection Create(int quantity) { if (quantity <= 0) throw new ArgumentException(); ProductCollection collection=new ProductCollection(); for (int i=0; i < quantity; i++) collection.Insert(new T()); return collection; } } ///两个实体批量生产工厂类型 public class BatchProductAFactory : BatchProductFactoryBase<ProductA> { } public class BatchProductBFactory : BatchProductFactoryBase<ProductB> { } }
3.5.4 增设生产指导顾问——Director
Director本身组织了一系列的Decision信息,它相当于指导客户程序调度不同实体工厂生产产品的指挥者,为了让客户程序的调用成为一个连续的过程,Director 可以采用迭代器组织每个Decision,这样客户程序就可以用一种连续的线性方式完成每个Decision所记录的生产任务了,其代码如下:
C# 定义Director和Decision
public abstract class DecisionBase { protected IBatchFactory factory; protected int quantity; public DecisionBase(IBatchFactory factory, int quantity) { this.factory=factory; this.quantity=quantity; } public virtual IBatchFactory Factory { get { return factory; } } public virtual int Quantity { get { return quantity; } } } public abstract class DirectorBase { protected IList<DecisionBase> decisions=new List<DecisionBase>(); ///实际项目中,最好将每个Director需要添加的Decision也定义在配置文件中, /// 这样增加新的Decision项都在后台完成,而不需要Assembler显式调用该方法补充 protected virtual void Insert(DecisionBase decision) { if ((decision == null) || (decision.Factory == null) || (decision.Quantity < 0)) throw new ArgumentException("decision"); decisions.Add(decision); } /// 便于客户程序使用增加的迭代器 public virtual IEnumerable<DecisionBase> Decisions { get { return decisions; } } }
3.5.5 由Director指导的客户程序
在完成了三个辅助类型(ProductCollection、Director、Decision)的设计之后,就可以设计由Director指导生产的新客户程序,示例代码如下:
C#
这个示例采用硬编码方式,没有通过Assembler获取Director class ProductADecision : DecisionBase { public ProductADecision() : base(new BatchProductAFactory(), 2) { } } class ProductBDecision : DecisionBase { public ProductBDecision() : base(new BatchProductBFactory(), 3) { } } class ProductDirector : DirectorBase { public ProductDirector() { base.Insert(new ProductADecision()); base.Insert(new ProductBDecision()); } } class Client { ///实际项目中,可以通过Assembler从外部把Director注入 private DirectorBase director=new ProductDirector(); public IProduct[] Produce() { ProductCollection collection=new ProductCollection(); foreach (DecisionBase decision in director.Decisions) collection += decision.Factory.Create(decision.Quantity); return collection.Data; } }
Unit Test
[TestClass] public class TestBatchFactory { [TestMethod] public void Test() { Client client=new Client(); IProduct[] products=client.Produce(); Assert.AreEqual<int>(2+3, products.Length); for (int i=0; i < 2; i++) Assert.AreEqual<string>("A", products[i].Name); for (int i=2; i < 5; i++) Assert.AreEqual<string>("B", products[i].Name); } }
通过上面的示例不难发现,虽然经典面向对象设计模式实现中并没有告诉我们如何批量创建对象,但通过增加新的监管对象(Director、Decision、ProductCollection)可以很容易地实现该需求。图3-8显示了该方式下的执行时序。
图3-8 批量工厂的执行时序
随着本书后续设计模式的介绍,你可以发现其实 Decision 就是一个Strategy,而Director本身可以作为客户程序的外部机制(Observer),异步指导客户程序的执行。
3.6 基于类型参数的Generic Factory
随着泛型的使用,IProduct的含义也被扩展了,项目中经常需要提供IProduct<T>,甚至IProduct<T1, T2, ….>之类的泛型抽象产品类型。这些类型在类库的最外层往往被赋予了具体的类型参数,但在内层类库部分,或者在高度抽象的通用算法部分,往往会继续保持泛型的抽象类型。为了保证工厂类型的加工过程的通用性,也需要设计具有泛型的Generic Factory。
为了说明使用泛型工厂方法的技术需要,我们看看下面这个假想的情形。
假想情景
应用中需要一个自定义的链表结构,它被称为ILinkList<T> (T为string, 值类型或实现了IComparable接口的类型),区别于我们通常向链表插入节点的时候已经按照键值作了升序或降序的处理,它本身就增加在链表的尾部,并提供了一个Sort()方法,用于将自己内部节点进行排序。客户程序有时候用它保存一个学生历年的语文成绩,语法为ILinkList<double>,有时候用它保存一个班级内学生姓名,语法为ILinkList<string>,但其 Soft()方法是通用的。如果我们要实现ILinkList<string>或ILinkList<double>的实体工厂类型,有两种选择:
● 选择一:定义一个ChineseMarkFactory和一个StudentNameFactory工厂,分别生成ILinkList<double>和ILinkList<string>。
● 选择二:定义一个LinkListFactory<T>,由客户程序根据需要提供具体的类型参数。
两种方式哪个更好呢?相信您也发现了——“视情况而定”,也就是上面说的:如果直接服务于最外层业务逻辑,那么选择第一种方式最好;如果继续用于内层抽象算法描述,那么选择第二种方式好像可继续保持算法的通用性。
其实在上面的批量对象工厂部分已经遇到了类似问题,就是因为可能返回IProduct,可能返回ProductCollection,所以我们定义了IFactory和IBatchFactory,但既然它们令客户程序需要的类型选择延迟到子类,为什么就不能把非常通用的new()也延迟到自己的子类呢?那些工厂都是IFactory<T>的某个子类,大家都有一个通用的方法T Create(),这样的代码多整齐。这样在统一体系之下,工厂方法模式部分的各个工厂可以实现的代码如下:
C#
///抽象的泛型工厂类型 public interface IFactory<T> { T Create(); } public abstract class FactoryBase<T> : IFactory<T> where T : new() { /// 由于批量工厂的可能应用概率比较小,因此默认为实现单个产品的工厂 public virtual T Create() { return new T(); } } /// 生产单个产品的实体工厂 public class ProductAFactory : FactoryBase<ProductA> { } public class ProductBFactory : FactoryBase<ProductB> { } /// 生产批量产品工厂的抽象定义 public abstract class BatchFactoryBase<TCollection, TItem> : FactoryBase<TCollection> where TCollection : ProductCollection, new() where TItem : IProduct, new() { protected int quantity; public virtual int Quantity { set { this.quantity=value; } } public override TCollection Create() { if (quantity <= 0) throw new ArgumentException("quantity"); TCollection collection=new TCollection(); for (int i=0; i < quantity; i++) collection.Insert(new TItem()); return collection; } } /// 生产批量产品的实体工厂 public class BatchProductAFactory : BatchFactoryBase<ProductCollection, ProductA> { } public class BatchProductBFactory : BatchFactoryBase<ProductCollection, ProductB> { }
上例是选择后一种实现方式的结果,那么相对前一种方式而言,它有什么好处呢?
● 最大的好处是整个体系只有一个根对象IFactory<T>,整个体系统一。那么是不是上面的IBatchFactory(Create()方法返回 ProductCollection)和IFactory(Create()方法返回IProduct)就不能做到只有一个根呢?也不是,它们都可以继承自IFactory,只不过Create()方法返回一个object就可以了,但这样不利于客户程序的调用,因为返回结果是object。为了后续开发方便(也就是点一个点之后,通过IntelliSense,就可以把具体产品类型的属性列出来),还需要增加一个强制类型转换,检查工作在运行态完成,而不是在变异过程完成。
● 另一个好处是隐性的,如果项目规模比较大,常常会感到别扭的地方就在于实际加工方法的命名,如果不做任何约束,开发人员可以写成Create()、CreateInstance()或Constructor(),还有各种名目的CreateProducts()、CreateProductCollection()、CreateProductList()等,有些时候要靠猜才能确认。与其“大杂烩”,不如大家都规规矩矩地用一个统一的名称,当然,如果子类有特殊情况,可以自己增加,但最基本的方法名称大家都用一个。
● 如果您设计的是某种框架(也就是具有常说的那种IOC特质——“Don't call me, I'll call you”的类库),而且还常常会用到反射调用某个工厂生产产品,那么统一名称可以令您写出一个很通用的反射发现Create()的机制,否则这个事情就麻烦了。
除此之外,笔者偏爱这种方式的原因更单纯,因为它是泛型的,看起来更有抽象的味道,比较Cool。
3.7 委托工厂类型
相对而言,上面的实现还和经典的实现比较“贴”,毕竟加工的是一个类型对象,但在C#实现设计模式的时候,很多行为型和结构型模式需要的内容就是类的方法。受到语言的限制,Java等很多语言必须用一个接口,然后由工厂把接口加工出来,但 C#有更经济的解决办法——委托。
你需要的是某个特征的方法、好定义、好规则(即委托),我找到合适的就给你挂上。
结合前面章节的介绍,您已经看到委托可以做很多非常方便的调用,无论是同步的还是异步的、无论是点对点调用还是组播调用,但是还有很多人认为委托是对面向对象概念的破坏,因为骨子里它就是个方法指针,但.NET平台把它们包装为一个个对象,所以我们才可以很容易地实现很多异步机制和组播,而且执行过程中很多时候要比创建业务类型在效率上划算得多。“君子性非异也,善假于物也”,是否使用委托,您可根据需要和开发习惯自己选择。
这里提供一个最简单的Delegate工厂方法实现,上面提到的依赖注入机制、基于配置文件实现等工厂化实现技巧,与之非常类似,本书不赘述。其代码如下:
C#
/// 委托本质上就是对具体执行方法的抽象,它相当于Product的角色 public delegate int CalculateHandler (params int[] items); class Calculator { /// 这个方法相当于Delegate Factory看到的Concrete Product public int Add(params int[] items) { int result=0; foreach (int item in items) result += item; return result; } } /// Concrete Factory public class CalculateHandlerFactory : IFactory<CalculateHandler> { public CalculateHandler Create() { return (new Calculator()).Add; } }
Unit Test
[TestMethod] public void Test() { IFactory<CalculateHandler> factory=new CalculateHandlerFactory(); CalculateHandler handler=factory.Create(); Assert.AreEqual<int>(1+2+3, handler(1, 2, 3)); }
3.8 小结
经典工厂方法给我们设计应用一个很好的模式化new()思维,通过引入新的对象,在很大程度上解放了客户程序对具体类型的依赖,其方法就是延迟构造到子类,但依赖关系是始终存在的,解放一个对象的时候等于把麻烦转嫁到另一个对象上。为此,工程中往往最后把推不掉的依赖关系推到配置文件上,也就是推到.NET Framework 上,这样经典的工厂方法模式往往在项目中被演绎成“工厂方法+依赖注入+配置文件访问”的组合方式。
考虑到性能因素,或者干脆为了省去多次调用的麻烦,工程中往往会有生成一批对象的需要,比如生成具有某些配置选项的多个线程,这时候就会用到批量对象工厂。但如前面章节所讨论的,写Demo和完成一个项目是两码事,其中一个关键的因素就是人,为了让整个实施过程中工厂的实现“中规中矩”,有时候要统一一些内容,泛型工厂能从根上约束整个工厂体系的加工方法命名,也许它看起来需要适应一段时间,但在使用上起码多了个选择。
最后就是工厂要为委托服务的问题,与IProduct、Concrete Product的模式不同,委托自己就是抽象类型,工厂在创建的时候不是new()个类,关键是找到匹配的方法。