Factory pattern 工廠模式
距離上一篇 Strategy pattern 策略模式已經兩個多月了,一直拖到現在才生出這篇文章...orz。
其實這篇工廠模式應該要當作第一篇 Design pattern 的文章會比較好,因為這個模式很容易懂,但是工廠模式中又細分出一些其他類似的模式,例如抽象工廠模式,所以我把一些相關的資料都讀了一遍後,分三篇作介紹。
工廠模式最主要的精神就是將 new Class 這個動作另外封裝成一個 Factory Class,這個Factory Class 專門負責實體化這些類別。
特地這樣做有什麼好處呢?
舉個例子,假如我們現在有兩個繼承 Product 的類別,它們擁有共同的方法 Operation()
一般來講,我們如果要實體化 ProductA 或 ProductB 的話,會這樣寫:
這樣做有什麼缺點呢?
如果我 product 型別要換成 ProductB 的話,就需要把第7行的 new ProductA() 改成 new ProductB(),若是繼承 Product 的類別一多,以後程式碼的維護上會很麻煩。
簡單工廠模式 Simple Factory Pattern (又稱靜態工廠模式 Static Factory Pattern):
按照上圖,將擁有共同介面類別的實體化動作封裝在 Factory 內,程式就可以在執行時動態決定要實體化哪個類別了。
Factory class:
Client:
到這裡可以看看使用 Factory 後,和使用之前的程式碼做比較,最大的差別就是現在可以靠變數來決定要實體化的類別了,若是需要改成 ProductB 的話,只需要將 productName 的值改成 "ProductB" 就行了。
如果覺得上面的 Factory productFactory = new Factory(); 這行有點礙眼的話,還可以將程式碼更簡單話一點,就是把 Factory class 的 CreateProduct() 方法改成 static:
這樣就可以直接跳過 new Factory() 的步驟,直接呼叫 CreateProduct() 方法:
使用簡單工廠模式雖然方便,但也是有缺點的。
如果每次新增一個 Product 子類別後,都必須修改 Factory class 中 CreateProduct() 的 switch 判斷式的話,這樣做不符合物件導向設計的 Open-Closeed Principle(開放-封閉原則) 精神。
什麼是開放-封閉原則呢?
簡單說就是程式容易擴充新功能,但是不用修改原始碼的意思。
如果因為要擴充 ProductC class 進來,而修改了 Factory class 的話,這樣的做法並不是很好,所以比較正統的工廠模式有另一種寫法,就是接著下一篇要介紹的工廠方法模式。
工廠方法模式 Factory Method Pattern:
要解決簡單工廠模式的問題,工廠方法模式的做法是將 Factory 類別抽象化,讓每個 Product 子類別都有屬於自己的工廠類別,如上圖所示。
Factory interface:
ProductFactoryA class:
ProductFactoryB class:
Client:
這樣就解決簡單工廠的擴充問題了,如果需要增加 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:
ProductFactoryA class:
ProductFactoryB class:
Client:
這樣就完成抽象工廠模式了,將 Factory 抽象化後, Client端就能利用多型的方法實體化Product,而不需要知道具體的類別就能操作它,這就是抽象工廠的優點。
進階技巧:
到這邊其實已經把工廠模式都介紹完了,不過其實上面的抽象工廠還是有不完美的地方,例如我們現在加入了一個新的Product系列ProductC(可以想像成Linux版本的Office),那麼我們除了要寫 Product1C 跟 Product2C 這些基本的類別外,還要再為它們新增一個 ProductFactoryC 的類別,讓人覺得有一些麻煩。
這邊要介紹大話設計模式一書中作者提供的一個技巧,可以將工廠模式再修改的更加完美。
看到上圖後,可以發現我們將 Factory 的抽象化拿掉了,變回最初的簡單工廠模式,這時候的 Factory 會變成這樣:
Factory class:
這樣寫的話,之前才提到的開放-封閉原則的缺點不是又出現了嗎?
如果增加了一個新的 ProductC 類別的話,就需要在 Factory 裡面增加新的 switch case 分支條件,不容易擴充的問題就出現了。
這裡要介紹 C# 跟 JAVA 都有提供的一個機制,Reflection(反射),它可以直接依照 class 的名稱來實體化類別,我們直接來看看用Reflection機制修改後的 Factory 程式碼吧:
Factory class:
Client:
看到了嗎!?這樣就算加入了其他 Product 子類別後,也不需要修改 Factory 的程式碼,只要在 CreateProduct() 方法內傳入要實體化的類別名稱就可以了,相當方便吧。
範例檔案:
參考資料:
其實這篇工廠模式應該要當作第一篇 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() 方法內傳入要實體化的類別名稱就可以了,相當方便吧。
範例檔案:
參考資料:
留言
張貼留言