疑惑?改良? 从简单工厂到工厂方法
写在前面
最近一段时间在研究有关设计模式方面的文章,拜读了TerryLee以及吕震宇两位老师所写的设计模式方面的系列文章,收获颇丰,也让我对OOP,OOD有了新的理解和认识,不过在看到工厂方面的几篇文章时,却总有个地方想不通,望各位老师专家能替在下答疑解惑,以下是自己对工厂模式的一些理解以及提出的一种改良方案,不知道是否有人提出过类似的方案,如有雷同纯属巧合。有说得不对的地方还请批评指正。
从简单工厂说起
严格来讲简单工厂并不属于GOF的23个设计模式之一,但它之所以存在而被人们广泛认知的原因,我想是由于它在一定程度上简化了工厂方法(Factory Method)与抽象工厂(Abstract Factory)的模式,从而可以带给新手对于工厂模式设计的精髓最直观的认识,是不少人接触工厂模式的奠基石,理解了简单工厂的设计理念及实现方法再去理解相对复杂的工厂方法与抽象工厂有着水到渠成的效果。
先来看看简单工厂的类关系图:
简单工厂一般以如下方式实现:
1 public class SimpleFactory
2 {
3 /// <summary>
4 /// Get the right product base on the paoductcategory parameter.
5 /// </summary>
6 /// <param name="productCategory"></param>
7 /// <returns></returns>
8 public static IProduct CreateProduct(string productCategory)
9 {
10 switch (productCategory)
11 {
12 case "A":
13 return new ProductA();
14 case "B":
15 return new ProductB();
16 case "C":
17 return new ProductC();
18 default:
19 throw new Exception("Not a valid product category!");
20 return null;
21 }
22 }
23 }
客户对于简单工厂的使用
1 public class Client
2 {
3 public void Do()
4 {
5 //Create a productA
6 IProduct product = SimpleFactory.CreateProduct("A");
7 //Product doing its tasks.
8 product.ExecuteFunction1();
9 product.ExecuteFunction2();
10 product.ExecuteFunction3();
11 product.ExecuteFunction4();
12 //More tasks .
13 }
14 }
这时,如果客户打算改为生产产品B,那么他需要做的仅仅是修改传入工厂的参数,以告诉工厂需要生产什么而对于后面产品所实现的功能不需要做任何的修改。简单工厂模式的最大优点在于工场模式包含了必要的判断逻辑,可以根据客户的需求动态生成客户所需要的产品,而在客户方面,免除了客户对具体产品创建的依赖,一句话:简单工场实现了对责任的分割。
说完了优点再来说说缺点,如果现在新增加了一种产品称作ProductC怎么办?OK,我们添加类ProductC、实现IProduct接口(注意,这个是由于新的业务规则所带来的变化,是任何模式都无法避免的),下面就是需要让我们的简单工厂具有生产这种新产品的能力,如何实现?你自然会想到多添一条case语句不就完了?然而事情并没有那么简单,因为你在添加case语句的同时也就意味着你修改了工厂类本身—违反了设计原则中最重要的一条OCP原则。开放封闭原则要求避免直接修改类而是应当通过继承来扩展类的功能。此外过多的case语句也容易混乱逻辑,造成工厂类的稳定性下降。那么对于新产品的增加,怎样才能妥善的处理并且又不违反设计原则呢?于是—工厂方法(Factory Method)登场了。
也谈工厂方法
先来看看工厂方法(Factory Method)的类关系图:
工厂方法一般以如下方式实现:
1 public interface IFactory
2 {
3 IProduct CreateProduct();
4 }
5
6 /// <summary>
7 /// 专门负责生产ProductA的工厂
8 /// </summary>
9 public class ProductAFactory : IFactory
10 {
11 public IProduct CreateProduct()
12 {
13 return new ProductA();
14 }
15 }
16
17 /// <summary>
18 /// 专门负责生产ProductB的工厂
19 /// </summary>
20 public class ProductBFactory : IFactory
21 {
22 public IProduct CreateProduct()
23 {
24 return new ProductB();
25 }
26 }
我们再看看对于新产品ProductC,工厂方法模式所采取的应对策略。首先仍然是要添加新的类ProductC,然后添加用于生产新产品的工厂类ProductCFactory并且实现IFactory接口,这样在保证能够实例化新产品的同时又没有修改已有的代码….Perfect? 别急,这时我们再来看看客户的代码:
1 public class Client
2 {
3 public void Do()
4 {
5 //Create a ProductA
6 IFactory factory = new ProductAFactory();
7 //IFactory factory = new ProductBFactory();
8
9 IProduct product = factory.CreateProduct();
10 product.DoTask1();
11 product.DoTask2();
12 product.DoTask3();
13 product.DoTask4();
14 product.DoTask5();
15 //Do more tasks
16 }
17 }
这时你会发现,客户端需要决定实例化哪一个工厂来生产自己所需要的产品,判断逻辑仍然存在,仅仅是从简单工厂的内部逻辑判断移到了客户端代码进行逻辑判断。也就是说如果你要想最终生产出ProductC仍然需要修改代码,而只是这次修改的位置移到了客户端!
应对策略,很多人都知道这种问题的一个有效的解决方式是可以利用.Net的反射特性或其他IOC机制,具体做法是将需要实例化的工厂类名写到配置文件中,通过反射来读去,代码如下:
1 <appSettings>
2 <add key="FactoryName" value="ProductBFactory" />
3 </appSettings>
1 public class Client
2 {
3 public void Do()
4 {
5 string factoryName = ConfigurationSettings.AppSettings["FactoryName"];
6 IFactory factory = (IFactory)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + factoryName);
7
8 IProduct product = factory.CreateProduct();
9 product.DoTask();
10 }
11 }
到这里几乎可以说是对于对象创建的一个完美应对策略,然而….
新的疑问
有“好事”者提出,既然都用到了.Net的反射机制那么工厂类本身是否还有存在的必要呢?是否可以通过在配制文件中写入需要生产的产品名称直接反射出具体的产品实例,从而略去工厂生产的步骤?具体代码参照如下:
1 IProduct product = (IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName);
2 product.DoTask();
对于这个问题,虽然看起来他在代码上有了一定的简化但是我认为他忽略了工厂模式的另一个特点,它集中封装了对象的创建,体现了代码的重用性。
我们再来对比一下两段代码,未使用工厂:
1 IProduct product1 = (IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName);
2 product1.DoTask();
3
4 IProduct product2 = (IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName);
5 product2.DoTask();
6
7 IProduct product3 = (IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName);
8 product3.DoTask();
使用了工厂:
1 IFactory factory = (IFactory)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + factoryName);
2
3 IProduct product1 = factory.CreateProduct();
4 product1.DoTask();
5
6 IProduct product2 = factory.CreateProduct();
7 product2.DoTask();
8
9 IProduct product3 = factory.CreateProduct();
10 product3.DoTask();
可以看到没有使用工厂方法的代码段到处充斥了类似(IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName)这样的代码,首先它不够优雅,其次没有考虑代码的重用性,如果项目的程序集名称或者命名空间发生了改变呢?虽然这种情况在实际项目中很少发生,但是你可以考虑一下若真的发生这种情况,你又需要修改多少地方~。那么这就是最终的解决方案了么,在我看来工厂方法(Factory Method)仍然有值得商榷的地方。
首先,逻辑相对复杂,不易使用。是否每个人都可以随意的在项目中熟练的运用工厂方法模式呢?
其次,可以看到在每增加一种新产品类型的时候,我们都需要多添加一个生产该产品的工厂,这便带来了业务需求之外的工作量。下面….
新的解决方案
借助第三节中所提到的反射机制的运用,我思考着能否将反射机制与简单工厂相结合,仍然借助简单工厂的设计模式,而将其对产品的判断逻辑由原先的代码改为反射配置文件。首先参照以下代码。
改良后的简单工厂:
1 public class ImprovedFactory
2 {
3 public static IProduct CreateProduct()
4 {
5 string productName = ConfigurationSettings.AppSettings["ProductName"];
6 IProduct product = (IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method.improvement." + productName);
7 return product;
8 }
9 }
Ok,我们再看看客户端代码:
1 IProduct product = ImprovedFactory.CreateProduct();
2 product.DoTask();
同样是对于新产品ProductC,新的解决方法需要做哪些工作?
a) 添加类ProductC并实现IProduct接口,就像前面我说的,这是使用任何模式都无法避免的改变。
b) 在配置文件中多添加一条关于ProductC的配置信息。
看到了么?没有那么多复杂的抽象、具体工厂;不需要对新的产品添加新的工厂;不需要修改工厂类本身或是客户端代码;没有破坏任何设计原则。仅仅需要上面两步就完成一个新产品的添加及创建工作。
以上是我对一种新的解决方案的尝试,也许这种方案也有缺陷的地方,但是我暂时还没有想到具体缺陷有哪些,刚刚接触设计模式不久,很多想法还不够成熟,还希望各位老师前辈多加指点。