Factory pattern 工廠模式

距離上一篇 Strategy pattern 策略模式已經兩個多月了,一直拖到現在才生出這篇文章...orz。
其實這篇工廠模式應該要當作第一篇 Design pattern 的文章會比較好,因為這個模式很容易懂,但是工廠模式中又細分出一些其他類似的模式,例如抽象工廠模式,所以我把一些相關的資料都讀了一遍後,分三篇作介紹。

工廠模式最主要的精神就是將 new Class 這個動作另外封裝成一個 Factory Class,這個Factory Class 專門負責實體化這些類別。
特地這樣做有什麼好處呢?
舉個例子,假如我們現在有兩個繼承 Product 的類別,它們擁有共同的方法 Operation(),如下圖:



一般來講,我們如果要實體化 ProductA 或 ProductB 的話,會這樣寫:

   1: namespace FactoryPattern
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             Product product = new ProductA();
   8:  
   9:             product.Operation();
  10:         }
  11:     }
  12: }
這樣做有什麼缺點呢?
如果我 product 型別要換成 ProductB 的話,就需要把第7行的 new ProductA() 改成 new ProductB(),若是繼承 Product 的類別一多,以後程式碼的維護上會很麻煩。

簡單工廠模式 Simple Factory Pattern (又稱靜態工廠模式 Static Factory Pattern):



按照上圖,將擁有共同介面類別的實體化動作封裝在 Factory 內,程式就可以在執行時動態決定要實體化哪個類別了。

Factory class:
   1: namespace SimpleFactoryPattern
   2: {
   3:     public class Factory
   4:     {
   5:         public Product CreateProduct(string name)
   6:         {
   7:             switch (name)
   8:             {
   9:                 case "ProductA":
  10:                     return new ProductA();
  11:                 case "ProductB":
  12:                     return new ProductB();
  13:                 default:
  14:                     return null;
  15:             }
  16:         }
  17:     }
  18: }


Client:
   1: using System;
   2:  
   3: namespace SimpleFactoryPattern
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             // productName可以動態決定要實體化的類別
  10:             string productName = "ProductA";
  11:         
  12:             Factory productFactory = new Factory();
  13:         
  14:             Product product = productFactory.CreateProduct(productName);
  15:  
  16:             product.Operation();
  17:         }
  18:     }
  19: }

到這裡可以看看使用 Factory 後,和使用之前的程式碼做比較,最大的差別就是現在可以靠變數來決定要實體化的類別了,若是需要改成 ProductB 的話,只需要將 productName 的值改成 "ProductB" 就行了。

如果覺得上面的 Factory productFactory = new Factory(); 這行有點礙眼的話,還可以將程式碼更簡單話一點,就是把 Factory class 的 CreateProduct() 方法改成 static:

   1: public static Product CreateProduct(string name)

這樣就可以直接跳過 new Factory() 的步驟,直接呼叫 CreateProduct() 方法:

   1: static void Main(string[] args)
   2: {
   3:      string productName = "ProductA";
   4:                 
   5:      Product product = Factory.CreateProduct(productName);
   6:  
   7:      product.Operation();
   8: }

使用簡單工廠模式雖然方便,但也是有缺點的。
如果每次新增一個 Product 子類別後,都必須修改 Factory class 中 CreateProduct() 的 switch 判斷式的話,這樣做不符合物件導向設計的 Open-Closeed Principle(開放-封閉原則) 精神。
什麼是開放-封閉原則呢?
簡單說就是程式容易擴充新功能,但是不用修改原始碼的意思。
如果因為要擴充 ProductC class 進來,而修改了 Factory class 的話,這樣的做法並不是很好,所以比較正統的工廠模式有另一種寫法,就是接著下一篇要介紹的工廠方法模式


工廠方法模式 Factory Method Pattern:

要解決簡單工廠模式的問題,工廠方法模式的做法是將 Factory 類別抽象化,讓每個 Product 子類別都有屬於自己的工廠類別,如上圖所示。


Factory interface:
   1: namespace FactoryMethodPattern
   2: {
   3:     public interface Factory
   4:     {
   5:         Product CreateProduct();
   6:     }
   7: }


ProductFactoryA class:
   1: namespace FactoryMethodPattern
   2: {
   3:     public class ProductFactoryA : Factory
   4:     {
   5:         public Product CreateProduct()
   6:         {
   7:             return new ProductA();
   8:         }
   9:     }
  10: }


ProductFactoryB class:
   1: namespace FactoryMethodPattern
   2: {
   3:     public class ProductFactoryB : Factory
   4:     {
   5:         public Product CreateProduct()
   6:         {
   7:             return new ProductB();
   8:         }
   9:     }
  10: }


Client:
   1: using System;
   2:  
   3: namespace FactoryMethodPattern
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             // ProductA
  10:             Factory productFactoryA = new ProductFactoryA();
  11:             Product productA = productFactoryA.CreateProduct();
  12:             productA.Operation();            
  13:             
  14:             // ProductB
  15:             Factory productFactoryB = new ProductFactoryB();
  16:             Product productB = productFactoryB.CreateProduct();            
  17:             productB.Operation();
  18:         }
  19:     }
  20: }

這樣就解決簡單工廠的擴充問題了,如果需要增加 ProductC 的話,只需要加入 繼承 Product 的ProductC class繼承 Factory 的 ProductFactoryC class 就可以了,不需要動到其他程式碼。


抽象工廠模式 Abstract Factory Pattern:

最後要介紹的是抽象工廠模式,這個模式其實跟上面的工廠方法是差不多的,只是增加了第二組Product的概念,我們來看看它的定義:
抽象工廠模式,提供一個建立一系列相關物件的介面,而無需指定它們具體的類別。
從上圖來看,我們有兩組產品,Product1 和 Product2,可以把它們想像成Office的Word和Excel,這兩組產品如果要跨平台到Mac的話,就會分支出Windows版和Mac版的,但是軟體做的事情是一樣的,所以它們繼承了共同的方法,如下圖:


看完上圖後,可以看出每個產品都有Windows和Mac兩種系列,所以我們可以為他們寫兩個工廠類別,一個專門負責生產Windows,一個專門負責生產Mac的產品,來看看程式碼:


Factory interface:
   1: namespace AbstractFactoryPattern
   2: {
   3:     public interface Factory
   4:     {
   5:         // 想像成CreateWindowsProduct()
   6:         Product1 CreateProduct1();
   7:  
   8:         // 想像成CreateMacProduct()
   9:         Product2 CreateProduct2();
  10:     }
  11: }


ProductFactoryA class:
   1: namespace AbstractFactoryPattern
   2: {
   3:     // 想像成WordFactory
   4:     public class ProductFactoryA : Factory
   5:     {
   6:         public Product1 CreateProduct1()
   7:         {
   8:             // 生產Windows的Word
   9:             return new Product1A();
  10:         }
  11:  
  12:         public Product2 CreateProduct2()
  13:         {
  14:             // 生產Mac的Word
  15:             return new Product2A();
  16:         }
  17:     }
  18: }


ProductFactoryB class:
   1: namespace AbstractFactoryPattern
   2: {
   3:     // 想像成ExcelFactory
   4:     public class ProductFactoryB : Factory
   5:     {
   6:         public Product1 CreateProduct1()
   7:         {
   8:             // 生產Windows的Excel
   9:             return new Product1B();
  10:         }
  11:  
  12:         public Product2 CreateProduct2()
  13:         {
  14:             // 生產Mac的Excel
  15:             return new Product2B();
  16:         }
  17:     }
  18: }


Client:
   1: namespace AbstractFactoryPattern
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             // Word工廠
   8:             Factory productFactoryA = new ProductFactoryA();            
   9:             // 製造Windows和Mac版本的Word
  10:             Product1 product1A = productFactoryA.CreateProduct1();            
  11:             Product2 product2A = productFactoryA.CreateProduct2();
  12:             // 跑兩種版本的Word
  13:             product1A.Operation();
  14:             product2A.Operation();
  15:  
  16:             // Excel工廠
  17:             Factory productFactoryB = new ProductFactoryB();
  18:             // 製造Windows和Mac版本的Excel
  19:             Product1 product1B = productFactoryB.CreateProduct1();
  20:             Product2 product2B = productFactoryB.CreateProduct2();
  21:             // 跑兩種版本的Excel
  22:             product2B.Operation();
  23:             product1B.Operation();
  24:         }
  25:     }
  26: }

這樣就完成抽象工廠模式了,將 Factory 抽象化後, Client端就能利用多型的方法實體化Product,而不需要知道具體的類別就能操作它,這就是抽象工廠的優點。


進階技巧:
到這邊其實已經把工廠模式都介紹完了,不過其實上面的抽象工廠還是有不完美的地方,例如我們現在加入了一個新的Product系列ProductC(可以想像成Linux版本的Office),那麼我們除了要寫 Product1C 跟 Product2C 這些基本的類別外,還要再為它們新增一個 ProductFactoryC 的類別,讓人覺得有一些麻煩。
這邊要介紹大話設計模式一書中作者提供的一個技巧,可以將工廠模式再修改的更加完美。

看到上圖後,可以發現我們將 Factory 的抽象化拿掉了,變回最初的簡單工廠模式,這時候的 Factory 會變成這樣:


Factory class:
   1: using System;
   2:  
   3: namespace SimpleAbstractFactoryPattern
   4: {
   5:     public class Factory
   6:     {
   7:         public static Product1 CreateProduct1(string name)
   8:         {
   9:             switch (name)
  10:             {
  11:                 case "Product1A":
  12:                     return new Product1A();
  13:                 case "Product1B":
  14:                     return new Product1B();
  15:                 default:
  16:                     throw new Exception();
  17:             }
  18:         }
  19:  
  20:         public static Product2 CreateProduct2(string name)
  21:         {
  22:             switch (name)
  23:             {
  24:                 case "Product2A":
  25:                     return new Product2A();
  26:                 case "Product2B":
  27:                     return new Product2B();
  28:                 default:
  29:                     throw new Exception();
  30:             }
  31:         }
  32:     }
  33: }

這樣寫的話,之前才提到的開放-封閉原則的缺點不是又出現了嗎?
如果增加了一個新的 ProductC 類別的話,就需要在 Factory 裡面增加新的 switch case 分支條件,不容易擴充的問題就出現了。
這裡要介紹 C# 跟 JAVA 都有提供的一個機制,Reflection(反射)它可以直接依照 class 的名稱來實體化類別,我們直接來看看用Reflection機制修改後的 Factory 程式碼吧:


Factory class:
   1: using System;
   2: using System.Reflection;
   3:  
   4: namespace SimpleAbstractFactoryPattern
   5: {
   6:     public class Factory
   7:     {
   8:         // 專案的namespace
   9:         private static readonly string AssemblyName = "SimpleAbstractFactoryPattern";
  10:  
  11:         public static Product1 CreateProduct1(string name)
  12:         {
  13:             // 如果傳進來的name是"Product1A"
  14:             // 那className就等於SimpleAbstractFactoryPattern.Product1A
  15:             string className = AssemblyName + "." + name;
  16:  
  17:             // 這裡就是Reflection,直接依照className實體化具體類別
  18:             return (IProduct1)Assembly.Load(AssemblyName).CreateInstance(className);
  19:         }
  20:  
  21:         public static Product2 CreateProduct2(string name)
  22:         {
  23:             string className = AssemblyName + "." + name;
  24:  
  25:             return (IProduct2)Assembly.Load(AssemblyName).CreateInstance(className);
  26:         }
  27:     }
  28: }


Client:
   1: namespace SimpleAbstractFactoryPattern
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             // 生產產品A
   8:  
   9:             Product1 product1A = Factory.CreateProduct1("Product1A");
  10:             Product2 product2A = Factory.CreateProduct2("Product2A");
  11:  
  12:             product1A.Operation();
  13:             product2A.Operation();
  14:  
  15:             // 生產產品B
  16:  
  17:             Product1 product1B = Factory.CreateProduct1("Product1B");
  18:             Product2 product2B = Factory.CreateProduct2("Product2B");
  19:  
  20:             product2B.Operation();
  21:             product1B.Operation();
  22:         }
  23:     }
  24: }

看到了嗎!?這樣就算加入了其他 Product 子類別後,也不需要修改 Factory 的程式碼,只要在 CreateProduct() 方法內傳入要實體化的類別名稱就可以了,相當方便吧。
範例檔案:
參考資料:

這個網誌中的熱門文章

DevOps:持續整合&持續交付(Docker、CircleCI、AWS)

如何優雅地在 Mac 上使用 dotfiles?