疑惑?改良? 從簡單工廠到工廠方法
寫在前面
最近一段時間在研究有關設計模式方面的文章,拜讀了TerryLee以及呂震宇兩位老師所寫的設計模式方面的系列文章,收獲頗豐,也讓我對OOP,OOD有了新的理解和認識,不過在看到工廠方面的幾篇文章時,卻總有個地方想不通,望各位老師專家能替在下答疑解惑,以下是自己對工廠模式的一些理解以及提出的一種改良方案,不知道是否有人提出過類似的方案,如有雷同純屬巧合。有說得不對的地方還請批評指正。
從簡單工廠說起
嚴格來講簡單工廠并不屬于GOF的23個設計模式之一,但它之所以存在而被人們廣泛認知的原因,我想是由于它在一定程度上簡化了工廠方法(Factory Method)與抽象工廠(Abstract Factory)的模式,從而可以帶給新手對于工廠模式設計的精髓最直觀的認識,是不少人接觸工廠模式的奠基石,理解了簡單工廠的設計理念及實現方法再去理解相對復雜的工廠方法與抽象工廠有著水到渠成的效果。
先來看看簡單工廠的類關系圖:
簡單工廠一般以如下方式實現:
public class SimpleFactory { /// /// Get the right product base on the paoductcategory parameter. /// /// /// public static IProduct CreateProduct(string productCategory) { switch (productCategory) { case "A": return new ProductA(); case "B": return new ProductB(); case "C": return new ProductC(); default: throw new Exception("Not a valid product category!"); return null; } } }
客戶對于簡單工廠的使用
public class Client { public void Do() { //Create a productA IProduct product = SimpleFactory.CreateProduct("A"); //Product doing its tasks. product.ExecuteFunction1(); product.ExecuteFunction2(); product.ExecuteFunction3(); product.ExecuteFunction4(); //More tasks . } }
這時,如果客戶打算改為生產產品B,那么他需要做的僅僅是修改傳入工廠的參數,以告訴工廠需要生產什么而對于后面產品所實現的功能不需要做任何的修改。簡單工廠模式的最大優點在于工場模式包含了必要的判斷邏輯,可以根據客戶的需求動態生成客戶所需要的產品,而在客戶方面,免除了客戶對具體產品創建的依賴,一句話:簡單工場實現了對責任的分割。
說完了優點再來說說缺點,如果現在新增加了一種產品稱作ProductC怎么辦?OK,我們添加類ProductC、實現IProduct接口(注意,這個是由于新的業務規則所帶來的變化,是任何模式都無法避免的),下面就是需要讓我們的簡單工廠具有生產這種新產品的能力,如何實現?你自然會想到多添一條case語句不就完了?然而事情并沒有那么簡單,因為你在添加case語句的同時也就意味著你修改了工廠類本身—違反了設計原則中最重要的一條OCP原則。開放封閉原則要求避免直接修改類而是應當通過繼承來擴展類的功能。此外過多的case語句也容易混亂邏輯,造成工廠類的穩定性下降。那么對于新產品的增加,怎樣才能妥善的處理并且又不違反設計原則呢?于是—工廠方法(Factory Method)登場了。
也談工廠方法
先來看看工廠方法(Factory Method)的類關系圖:
工廠方法一般以如下方式實現:
public interface IFactory { IProduct CreateProduct(); } /// /// 專門負責生產ProductA的工廠 /// public class ProductAFactory : IFactory { public IProduct CreateProduct() { return new ProductA(); } } /// /// 專門負責生產ProductB的工廠 /// public class ProductBFactory : IFactory { public IProduct CreateProduct() { return new ProductB(); } }
我們再看看對于新產品ProductC,工廠方法模式所采取的應對策略。首先仍然是要添加新的類ProductC,然后添加用于生產新產品的工廠類ProductCFactory并且實現IFactory接口,這樣在保證能夠實例化新產品的同時又沒有修改已有的代碼….Perfect? 別急,這時我們再來看看客戶的代碼:
public class Client { public void Do() { //Create a ProductA IFactory factory = new ProductAFactory(); //IFactory factory = new ProductBFactory(); IProduct product = factory.CreateProduct(); product.DoTask1(); product.DoTask2(); product.DoTask3(); product.DoTask4(); product.DoTask5(); //Do more tasks } }
這時你會發現,客戶端需要決定實例化哪一個工廠來生產自己所需要的產品,判斷邏輯仍然存在,僅僅是從簡單工廠的內部邏輯判斷移到了客戶端代碼進行邏輯判斷。也就是說如果你要想最終生產出ProductC仍然需要修改代碼,而只是這次修改的位置移到了客戶端!
應對策略,很多人都知道這種問題的一個有效的解決方式是可以利用.Net的反射特性或其他IOC機制,具體做法是將需要實例化的工廠類名寫到配置文件中,通過反射來讀去,代碼如下:
<appSettings> <add key="FactoryName" value="ProductBFactory" /> <appSettings>
public class Client { public void Do() { string factoryName = ConfigurationSettings.AppSettings["FactoryName"]; IFactory factory = (IFactory)Assembly.Load("Factory Method").CreateInstance( "Factory_Method." + factoryName); IProduct product = factory.CreateProduct(); product.DoTask(); } }
到這里幾乎可以說是對于對象創建的一個完美應對策略,然而….
新的疑問
有“好事”者提出,既然都用到了.Net的反射機制那么工廠類本身是否還有存在的必要呢?是否可以通過在配制文件中寫入需要生產的產品名稱直接反射出具體的產品實例,從而略去工廠生產的步驟?具體代碼參照如下:
IProduct product = (IProduct)Assembly.Load("Factory Method") .CreateInstance("Factory_Method." + productName); product.DoTask();
對于這個問題,雖然看起來他在代碼上有了一定的簡化但是我認為他忽略了工廠模式的另一個特點,它集中封裝了對象的創建,體現了代碼的重用性。
我們再來對比一下兩段代碼,未使用工廠:
IProduct product1 = (IProduct)Assembly.Load("Factory Method") .CreateInstance("Factory_Method." + productName); product1.DoTask(); IProduct product2 = (IProduct)Assembly.Load("Factory Method") .CreateInstance("Factory_Method." + productName); product2.DoTask(); IProduct product3 = (IProduct)Assembly.Load("Factory Method") .CreateInstance("Factory_Method." + productName); product3.DoTask();
使用了工廠:
IFactory factory = (IFactory)Assembly.Load("Factory Method") .CreateInstance("Factory_Method." + factoryName); IProduct product1 = factory.CreateProduct(); product1.DoTask(); IProduct product2 = factory.CreateProduct(); product2.DoTask(); IProduct product3 = factory.CreateProduct(); product3.DoTask();
可以看到沒有使用工廠方法的代碼段到處充斥了類似(IProduct)Assembly.Load("Factory Method").CreateInstance("Factory_Method." + productName)這樣的代碼,首先它不夠優雅,其次沒有考慮代碼的重用性,如果項目的程序集名稱或者命名空間發生了改變呢?雖然這種情況在實際項目中很少發生,但是你可以考慮一下若真的發生這種情況,你又需要修改多少地方~。那么這就是最終的解決方案了么,在我看來工廠方法(Factory Method)仍然有值得商榷的地方。
首先,邏輯相對復雜,不易使用。是否每個人都可以隨意的在項目中熟練的運用工廠方法模式呢?
其次,可以看到在每增加一種新產品類型的時候,我們都需要多添加一個生產該產品的工廠,這便帶來了業務需求之外的工作量。下面….
新的解決方案
借助第三節中所提到的反射機制的運用,我思考著能否將反射機制與簡單工廠相結合,仍然借助簡單工廠的設計模式,而將其對產品的判斷邏輯由原先的代碼改為反射配置文件。首先參照以下代碼。
改良后的簡單工廠:
public class ImprovedFactory { public static IProduct CreateProduct() { string productName = ConfigurationSettings.AppSettings["ProductName"]; IProduct product = (IProduct)Assembly.Load("Factory Method") .CreateInstance("Factory_Method.improvement." + productName); return product; } }
Ok,我們再看看客戶端代碼:
IProduct product = ImprovedFactory.CreateProduct(); product.DoTask();
同樣是對于新產品ProductC,新的解決方法需要做哪些工作?
a) 添加類ProductC并實現IProduct接口,就像前面我說的,這是使用任何模式都無法避免的改變。
b) 在配置文件中多添加一條關于ProductC的配置信息。
看到了么?沒有那么多復雜的抽象、具體工廠;不需要對新的產品添加新的工廠;不需要修改工廠類本身或是客戶端代碼;沒有破壞任何設計原則。僅僅需要上面兩步就完成一個新產品的添加及創建工作。
以上是我對一種新的解決方案的嘗試,也許這種方案也有缺陷的地方,但是我暫時還沒有想到具體缺陷有哪些,剛剛接觸設計模式不久,很多想法還不夠成熟,還希望各位老師前輩多加指點。