创建型模式是为了隔离客户程序与具体类型实例化的依赖关系,通过将实例化职责委托他方法对象的方法,保证客户程序(或外部系统)获得期望具体类型实例的同时不必发生直接的引用。
概念:
工厂方法是整个创建型模式中最为典型的、也是最具启发效果的,它告诉我们使用一个变化频率比较高的类不比忙着new(),而是要依赖一个抽象的类型(抽象类或者接口)。Delegate也是一个抽象,与抽象类型不同,它是对一类方法的抽象,而不像前两者是对一组方法的抽象。哪一种更好呢。
尺有所长,寸有所短。
如果需要的仅仅是某个特定的操作,那么大可不必按照抽象类型来加工,反馈一个Delegate实例就可以了。如果需要的是抽象业务实体,或者是具有一组“操作+属性”的抽象类型,那么就循规蹈矩好了。
使用工厂方法的主要动机来自于“变化”,应用的哪些部分会快速变化呢?不一定,多数项目都会一个相对稳定的核心。无论是被叫做Framework,还是现在更时髦的、感觉更上层的Foundation、Core等等。这个部分相对比较稳定,言外之意其他部分会“相对”变化的比较频繁。
我们可能期待架构师设计一个很灵活的架构,这样开发的时候就可以“填空”,而且是Plug&&Play方法的:我们也可以期待需求分析人员把需求分析得特别透彻,这样我们代码真的Write Once Run Always了。当然,几人是Team工作,就有上下家,周围还有共同努力的同事们,我们希望上家的变化尽可能的少,同时处于人际工程学的需要,我们也尽量不为下家找麻烦。
很难。
基于几口的开发虽然不能解决上述那么多问题,但起码可以从很多方面减轻这些工作负担。如果想贯彻这种思想,那么首先要从类型结构上解决问题。否则后面的都是空谈。(当然,可以通过正交的方法,或者一般成为“拦截”的方式在具体方法执行上提供灵活性。)
最简单的工厂:
public class Simple { public interface IProduct { } public class ConcreteProductA : IProduct { } public class ConcreteProductB : IProduct { } public class Factory { //由工厂决定到底实例话哪个子类。 public IProduct Create() { return new ConcreteProductA(); } } }
单元测试:
[TestMethod] public void TestMethod1() { var factory = new Simple.Factory(); var product = factory.Create(); Assert.AreEqual(product.GetType(), typeof(Simple.ConcreteProductA)); }
这个工厂模式与直接New()有什么不同?加入把它放到工程中应用有是不同的地方?
1.通过IProduct隔离了客户程序与具体ConcreteProdcutX的依赖关系,在客户程序视野内根本就没有ConcreteProductX.
2.即使ConcreteProductX增加、删除方法或者属性,也没有关系。只要按照要求实现了Iproduct就可以 ,Client无须关心ConcreteProductX的变化 (确切的说它也关系不到。。。)
3.想对直接写个ConcreteProductX而言,要平白的多写一个工厂出来,尤其当需要Iproduct频繁变化的时候,客户程序也闲不下来。
4.好的需求分析师在可以实施之前分析清楚85%的需求,好的架构师在把这些需求转换为实际技术框架的时候大概能做到90%的忠于需求,作为开发人员,设计的时候能够详细刻画95%的内容就很不错了,这样100%-85%*90%*95%=27.3%,也就是说,即使在一个精英团队,也可能四分之一的内容到了编码的时候还无法得到精确分析,那么对应某个局部领域而言,很难有效抽象这个Iproduct;但Factory提示在第一遍Draft的时候,我们就可以直接New(),但在复查或迭代的过程中一定要尽量找到Iproduct,然后套个Factory,在公共代码部分,更是如此。
此外,还有个效率的问题,如果构造一个Factory实例,并用它获取一个抽象类型实例后就不再使用,资源上有些浪费,尤其在这个工程非常频繁的时候。工程中可以通过如下集中方式解决:
1.把工厂实例作为参数注入到操作中,而不是在每个方法内部自行创建,类似:
private string _connectionString; private DbProviderFactory _dbProviderFactory; protected DataBase(string connectionString, DbProviderFactory dbProviderFactory) { _connectionString = connectionString; _dbProviderFactory = dbProviderFactory; } public virtual DbConnection CreateConnection() { DbConnection newConnection = _dbProviderFactory.CreateConnection(); newConnection.ConnectionString = _connectionString; return newConnection; }
2.把工厂设计为单例模式,因为工厂的职责相对单一,所有客户端需要的加工过程使用的都是一个唯一的共享实例。
3.使用静态类,不过注意:静态类不能被继承,它只能从object继承,没有其他可能。所以看起来静态类有些不想“面向对象”的味道。
public enum Category { A, B } public static class ProductFactory { public static Simple.IProduct Create(Category category) { switch (category) { case Category.A: return new Simple.ConcreteProductA(); case Category.B: return new Simple.ConcreteProductB(); default: throw new NotSupportedException(); } } }
参数化工厂
上面的那个简单工厂,只能是个Example,充其量是个模型。因为我们有两个实现类,如果没有任何机制告诉Factory该构造谁,这个工厂只能采用上面那个A或者随机产生A/B之中某一个实例的方法,这是不可以的,如果想让工厂在运行的时候有效选择符合抽象类型要求的某个实例,最简单的就是传递一个参数。它可以是一个字符串,一个枚举值等等。
简单工厂的局限性
上面的简单工厂比较优雅的解决了外部new()的问题,它把目标实例的创建工作交给一个外部的工厂完成,但是如果应用中需要工厂的类型只有一个,而且工厂的职责又非常单纯——就是一个new()的替代品,类似我们面向对象中最普遍的思路,这时候就需要进一步抽象了,于是出现了新的发展:工厂方法模式和抽象工厂模式。
工厂方法模式:
作为简单工厂的一个扩展,工厂方法的意图也非常明确,它把类的实例化过程延迟到子类,将new()的工作交给工厂完成。同时,增加一个抽象的工厂定义,解决一系列具有统一通用工厂方法的实体工厂问题。它适合以下场景。
1.客户程序需要隔离它与需要创建的具体类型间的耦合关系。
2.很多情况下,客户程序在开发过程中还无法预知生产环境中实际要提供给客户程序创建的具体类型。
3.将创建型工作隔离在客户程序之外,客户程序仅需要执行自己的业务逻辑,把这部分职责交给外部对象完成。
4.如果目标对象虽然可以统一抽象为某个抽象类型,但他们的继承关系太过复杂,层次性比较复杂。
经典工厂方法主要有四个角色。
1.抽象产品类型(Product):工厂要加工的对象所具有的抽象特征实体。
2。具体产品类型(Concrete Product) :实现客户程序所需要的抽象特质的类型,它就是工厂需要延迟实例的备选对象。
3.声明的抽象工厂类型(Creator):定义一个工厂方法的默认实现,它返回抽象产品类型Product.
4.重新定义创建过程的具体工厂类型(Concrete Creator),返回具体产品类型Concrete Product的具体工厂。
代码如下:
/// <summary> /// 抽象产品类型 /// </summary> public interface IProduct { string Name { get; }//约定的抽象产品所必须具有的特征 } //具体产品类型 public class ProductA : IProduct { public string Name => "A"; } public class ProductB : IProduct { public string Name => "B"; } public interface IFactory//抽象工厂 { IProduct Create();//每个工厂所需要具有的工厂方法--创建产品 } //抽象工厂的实例 public class FactoryA : IFactory { public IProduct Create() { return new ProductA(); } } public class FactoryB : IFactory { public IProduct Create() { return new ProductB(); } }
通过这个例子可以看到,当客户程序作为Iproduct消费者的时候,抽象类型在虚拟构造的时候到底用哪个实体类型由Factory决定。客户程序需要使用Iproduct的时候通过访问Ifactory所定义的Create()方法就可以获得一个产品实例。直观上客户程序可以把频繁变化的某个Product隔离在自己的视线之外,自身不会收到它的影响。
解耦Concrete Factory与客户程序
虽然经典工厂方法模式告诉了我们最后的结果,通过Prodcut、Concrete Product、Factory和Concrete Factory四个角色解决了客户程序对Product的获取问题,但没有把客户程序和Factory连起来,也就是说,没有介绍如何把Factory放到客户程序里。示例代码如下:
public class Client { public void SomeMethod() { IFactory factory = new FactoryA();//获取了抽象的Factory的同时与FactoryA产生了依赖。 IProduct product = factory.Create();//后续操作仅依赖抽象的IFactory和Iprodcut。 } }
如果让客户程序来直接new()某个Concrete Factory,由于Concrete Factor依赖与Concrete Product,因此还是形成了间接的实体对象依赖关系,背离了这个模式的初始意图。怎么办?
依赖注入,通过依赖注入将客户程序需要的某个IFactory传递给它。
实际工程中很多时候不得不做这种工作——将Concrete Product与客户程序分离,其原因是我们不想让上游开发人员的修改迫使我们必须重新Check Out代码,编译、测试再Check in,这种工作在项目开发的中后期常常是令人非常厌恶的,所以,我们把这些烂摊子交给依赖注入。
以下是实现依赖注入的Assembler的代码。
public class Assembler //一个手动实现依赖注入的中间部分 { /// <summary> /// 保存"抽象类型/实体类型"对应关系的字典 /// </summary> private static readonly Dictionary<Type, Type> Dictionary = new Dictionary<Type, Type>(); static Assembler() { //注册抽象类型需要使用的实体类型 //实际的配置信息可以从外层机制获取,例如通过配置获取 //Dictionary.Add(typeof(ITimerProvider), typeof(SystemTimeprovider)); Dictionary.Add(typeof (IFactory), typeof (FactoryA));//增加一个注册项 } /// <summary> /// 根据客户程序需要的抽象类型选择相应的实体类型,并返回类型实例 /// </summary> /// <param name="type">实体类型实例</param> /// <returns></returns> public object Create(Type type)//主要用于非泛型方式调用 { if ((type == null) || !Dictionary.ContainsKey(type)) throw new NullReferenceException(); var targetType = Dictionary[type]; return Activator.CreateInstance(targetType); } /// <summary> /// 抽象类型(抽象类/接口/或者某种基类) /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T Create<T>()//主要用于泛型方式调用 { return (T)Create(typeof(T)); } }
相应的客户程序就可以修改为依赖Assember的方式
public class ClientA { private readonly IFactory _factory; public ClientA(IFactory factory)//通过构造器方式注入 { if (factory == null) throw new ArgumentNullException(); _factory = factory; } public IProduct GetProduct() => _factory.Create(); }
单元测试代码:
public void Test() { var factory = (new Assembler()).Create<IFactory>(); var clienta = new ClientA(factory);//注入IFactory var product = clienta.GetProduct();//通过IFactory获取IProduct Assert.AreEqual("A", product.Name); }
这样的话,即使是在系统上线之后,如果还需要修改IFactory的具体类型,一样可以通过增加新的程序集,在生产环境中更新相关IFactory需要使用的具体工厂类型。在这种方式下,客户程序成为一个可以动态加载的框架,外部机制很容易通过配置将新完成的程序集部署到运行环境中,而在经典的设计模式实现中,如果需要把模式工程话,很多时候需要借助外部的配置。
学习内容,记录下来。