Factory pattern 工廠模式

距離上一篇 Strategy pattern 策略模式 已經兩個多月了,一直拖到現在才生出這篇文章...orz。

其實這篇工廠模式應該要當作第一篇 Design pattern 的文章會比較好,因為這個模式很容易懂,但是工廠模式中又細分出一些其他類似的模式,例如抽象工廠模式,所以我把一些相關的資料都讀了一遍後,分三篇作介紹。

工廠模式最主要的精神就是將 new Class 這個動作另外封裝成一個 Factory Class,這個 Factory Class 專門負責實體化這些類別。

特地這樣做有什麼好處呢?

舉個例子,假如我們現在有兩個繼承 Product 的類別,它們擁有共同的方法 Operation()

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

namespace FactoryPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			Product product = new ProductA();

			product.Operation();
		}
	}
}

這樣做有什麼缺點呢?

如果我 product 型別要換成 ProductB 的話,就需要把第 7 行的 new ProductA() 改成 new ProductB(),若是繼承 Product 的類別一多,以後程式碼的維護上會很麻煩。

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

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

Factory class:

namespace SimpleFactoryPattern
{
	public class Factory
	{
		public Product CreateProduct(string name)
		{
			switch (name)
			{
				case "ProductA":
					return new ProductA();
				case "ProductB":
					return new ProductB();
				default:
					return null;
			}
		}
	}
}

Client:

using System;
 
namespace SimpleFactoryPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			// productName可以動態決定要實體化的類別
			string productName = "ProductA";
        
			Factory productFactory = new Factory();
        
			Product product = productFactory.CreateProduct(productName);
 
			product.Operation();
		}
	}
}

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

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

public static Product CreateProduct(string name)

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

static void Main(string[] args)
{
	string productName = "ProductA";
                
	Product product = Factory.CreateProduct(productName);
 
	product.Operation();
}

使用簡單工廠模式雖然方便,但也是有缺點的。

如果每次新增一個 Product 子類別後,都必須修改 Factory class 中 CreateProduct() 的 switch 判斷式的話,這樣做不符合物件導向設計的 Open-Closeed Principle(開放-封閉原則) 精神。

什麼是開放-封閉原則呢?

簡單說就是程式容易擴充新功能,但是不用修改原始碼的意思。

如果因為要擴充 ProductC class 進來,而修改了 Factory class 的話,這樣的做法並不是很好,所以比較正統的工廠模式有另一種寫法,就是接著下一篇要介紹的工廠方法模式

工廠方法模式 Factory Method Pattern:

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

Factory interface:

namespace FactoryMethodPattern
{
	public interface Factory
	{
		Product CreateProduct();
	}
}

ProductFactoryA class:

namespace FactoryMethodPattern
{
	public class ProductFactoryA : Factory
	{
		public Product CreateProduct()
		{
			return new ProductA();
		}
	}
}

ProductFactoryB class:

namespace FactoryMethodPattern
{
	public class ProductFactoryB : Factory
	{
		public Product CreateProduct()
		{
			return new ProductB();
		}
	}
}

Client:

using System;
 
namespace FactoryMethodPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			// ProductA
			Factory productFactoryA = new ProductFactoryA();
			Product productA = productFactoryA.CreateProduct();
			productA.Operation();            
            
			// ProductB
			Factory productFactoryB = new ProductFactoryB();
			Product productB = productFactoryB.CreateProduct();            
			productB.Operation();
		}
	}
}

這樣就解決簡單工廠的擴充問題了,如果需要增加 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:

namespace AbstractFactoryPattern
{
	public interface Factory
	{
		// 想像成CreateWindowsProduct()
		Product1 CreateProduct1();
 
		// 想像成CreateMacProduct()
		Product2 CreateProduct2();
	}
}

ProductFactoryA class:

namespace AbstractFactoryPattern
{
	// 想像成WordFactory
	public class ProductFactoryA : Factory
	{
		public Product1 CreateProduct1()
		{
			// 生產Windows的Word
			return new Product1A();
		}
 
		public Product2 CreateProduct2()
		{
			// 生產Mac的Word
			return new Product2A();
		}
	}
}

ProductFactoryB class:

namespace AbstractFactoryPattern
{
	// 想像成ExcelFactory
	public class ProductFactoryB : Factory
	{
		public Product1 CreateProduct1()
		{
			// 生產Windows的Excel
			return new Product1B();
		}
 
		public Product2 CreateProduct2()
		{
			// 生產Mac的Excel
			return new Product2B();
		}
	}
}

Client:

namespace AbstractFactoryPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			// Word工廠
			Factory productFactoryA = new ProductFactoryA();            
			// 製造Windows和Mac版本的Word
			Product1 product1A = productFactoryA.CreateProduct1();            
			Product2 product2A = productFactoryA.CreateProduct2();
			// 跑兩種版本的Word
			product1A.Operation();
			product2A.Operation();
 
			// Excel工廠
			Factory productFactoryB = new ProductFactoryB();
			// 製造Windows和Mac版本的Excel
			Product1 product1B = productFactoryB.CreateProduct1();
			Product2 product2B = productFactoryB.CreateProduct2();
			// 跑兩種版本的Excel
			product2B.Operation();
			product1B.Operation();
		}
	}
}

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

進階技巧:

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

這邊要介紹大話設計模式一書中作者提供的一個技巧,可以將工廠模式再修改的更加完美。

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

Factory class:

using System;
 
namespace SimpleAbstractFactoryPattern
{
	public class Factory
	{
		public static Product1 CreateProduct1(string name)
		{
			switch (name)
			{
				case "Product1A":
					return new Product1A();
				case "Product1B":
					return new Product1B();
				default:
					throw new Exception();
			}
		}
 
		public static Product2 CreateProduct2(string name)
		{
			switch (name)
			{
				case "Product2A":
					return new Product2A();
				case "Product2B":
					return new Product2B();
				default:
					throw new Exception();
			}
		}
	}
}

這樣寫的話,之前才提到的開放-封閉原則的缺點不是又出現了嗎?

如果增加了一個新的 ProductC 類別的話,就需要在 Factory 裡面增加新的 switch case 分支條件,不容易擴充的問題就出現了。

這裡要介紹 C# 跟 JAVA 都有提供的一個機制,Reflection(反射)它可以直接依照 class 的名稱來實體化類別,我們直接來看看用Reflection機制修改後的 Factory 程式碼吧:

Factory class:

using System;
using System.Reflection;
 
namespace SimpleAbstractFactoryPattern
{
	public class Factory
	{
		// 專案的namespace
		private static readonly string AssemblyName = "SimpleAbstractFactoryPattern";
 
		public static Product1 CreateProduct1(string name)
		{
			// 如果傳進來的name是"Product1A"
			// 那className就等於SimpleAbstractFactoryPattern.Product1A
			string className = AssemblyName + "." + name;
 
			// 這裡就是Reflection,直接依照className實體化具體類別
			return (IProduct1)Assembly.Load(AssemblyName).CreateInstance(className);
		}
 
		public static Product2 CreateProduct2(string name)
		{
			string className = AssemblyName + "." + name;
 
			return (IProduct2)Assembly.Load(AssemblyName).CreateInstance(className);
		}
	}
}

Client:

namespace SimpleAbstractFactoryPattern
{
	class Program
	{
		static void Main(string[] args)
		{
			// 生產產品A
 
			Product1 product1A = Factory.CreateProduct1("Product1A");
			Product2 product2A = Factory.CreateProduct2("Product2A");
 
			product1A.Operation();
			product2A.Operation();
 
			// 生產產品B
 
			Product1 product1B = Factory.CreateProduct1("Product1B");
			Product2 product2B = Factory.CreateProduct2("Product2B");
 
			product2B.Operation();
			product1B.Operation();
		}
	}
}

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

範例檔案:

參考資料: