設計模式的征途—3.工廠方法(Factory Method)模式
上一篇的簡單工廠模式雖然簡單,但是存在一個很嚴重的問題:當系統中需要引入新產品時,由于靜態工廠方法通過所傳入參數的不同來創建不同的產品,這必定要修改工廠類的源代碼,將違背開閉原則。如何實現新增新產品而不影響已有代碼?工廠方法模式為此應運而生。
| 工廠方法模式(Factory Method) | 學習難度:★★☆☆☆ | 使用頻率:★★★★★ |
一、簡單工廠版的日志記錄器
1.1 軟件需求說明
Requirement:M公司欲開發一個系統運行日志記錄器(Logger),該記錄器可以通過多種途徑保存系統的運行日志,例如通過文件記錄或數據庫記錄,用戶可以通過修改配置文件靈活地更換日志記錄方式。在設計各類日志記錄器時,M公司的開發人員發現需要對日志記錄器進行一些初始化工作,初始化參數的攝制過程比較復雜,而且某些參數的設置有嚴格的先后次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程并保證多種記錄器切換的靈活性是M公司開發人員面臨的一個難題。
M公司開發人員學習了簡單工廠模式對日志記錄器進行了設計,初始結構如下圖所示。

1.2 基于簡單工廠的代碼實現
M公司的程序猿按照結構圖,寫下了核心代碼LoggerFactory的CreateLogger方法:
// 簡單工廠方法 public static ILogger CreateLogger(string args) { if (args.Equals("db", StringComparison.OrdinalIgnoreCase)) { // 連接數據庫,代碼省略 // 創建數據庫日志記錄器對象 ILogger logger = new DatabaseLogger(); // 初始化數據庫日志記錄器,代碼省略 return logger; } else if(args.Equals("file", StringComparison.OrdinalIgnoreCase)) { // 創建日志文件,代碼省略 // 創建文件日志記錄器對象 ILogger logger = new FileLogger(); // 初始化文件日志記錄器,代碼省略 return logger; } else { return null; } }
上述代碼省略了具體日志記錄器類的初始化代碼,在LoggerFactory中提供了靜態工廠方法CreateLogger(),用于根據所傳入的參數創建各種不同類型的日志記錄器。通過使用簡單工廠模式,將日志記錄器對象的創建和使用分離,客戶端只需要使用由工廠類創建的日志記錄器對象即可,無須關心對象的創建過程。
But,雖然簡單工廠模式實現了對象的創建和使用分離,仍然存在以下兩個問題:
(1)工廠類過于龐大!包含了大量的if-else代碼,維護和測試的難度增大不少。
(2)系統擴展不靈活,如果新增類型的日志記錄器,必須修改靜態工廠方法的業務邏輯,違反了開閉原則。
如何解決這兩個問題,M公司程序猿苦思冥想,想要改進簡單工廠模式,于是開始學習工廠方法模式。
二、工廠方法模式介紹
2.1 工廠方法模式概述
在簡單工廠模式中只提供一個工廠類,該工廠類需要知道每一個產品對象的創建細節,并決定合適實例化哪一個產品類。其最大的缺點就是當有新產品加入時,必須修改工廠類,需要在其中加入必要的業務邏輯,這違背了開閉原則。此外,在簡單工廠模式中,所有的產品都由同一個工廠創建,工廠類職責較重,業務邏輯較為復雜,具體產品與工廠類之間的耦合度較高,嚴重影響了系統的靈活性和擴展性。
在工廠方法模式中,不再提供一個統一的工廠類來創建所有的產品對象,而是針對不同的產品提供不同的工廠,系統提供一個與產品等級結構對應的工廠等級結構。
工廠方法(Factory Method)模式:定義一個用于創建對象的接口,讓子類決定將哪一個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱為工廠模式,也可稱為多態工廠模式,它是一種創建型模式。
2.2 工廠方法模式結構圖
工廠方法模式提供一個抽象工廠接口來聲明抽象工廠方法,而由其子類來具體實現工廠方法并創建具體的產品對象。

從圖中可以看出,在工廠方法模式結構圖中包含以下4個角色:
(1)Product(抽象產品):定義產品的接口,是工廠方法模式所創建的對象的超類,也就是產品對象的公共父類。
(2)ConcreteProduct(具體產品):它實現了抽象產品接口,某種類型的具體產品由專門的具體工廠創建,具體工廠和具體產品之間一一對應。
(3)Factory(抽象工廠):抽象工廠類,聲明了工廠方法,用于返回一個產品。
(4)ConcreteFactory(具體工廠):抽象工廠的子類,實現了抽象工廠中定義的工廠方法,并可由客戶端調用,返回一個具體產品類的實例。
三、工廠方法版的日志記錄器
3.1 解決方案
M公司的程序猿學習了工廠方法之后,決定使用工廠方法模式來重構設計,其基本結構圖如下圖所示:

其中, Logger接口充當抽象產品角色,而FileLogger和DatabaseLogger則充當具體產品角色。LoggerFactory接口充當抽象工廠角色,而FileLoggerFactory和DatabaseLoggerFactory則充當具體工廠角色。
3.2 重構代碼
(1)抽象產品:ILogger接口
public interface ILogger { void WriteLog(); }
(2)具體產品:FileLogger和DatabaseLogger類
public class FileLogger : ILogger { public void WriteLog() { Console.WriteLine("文件日志記錄..."); } } public class DatabaseLogger : ILogger { public void WriteLog() { Console.WriteLine("數據庫日志記錄..."); } }
(3)抽象工廠:ILoggerFactory接口
public interface ILoggerFactory { ILogger CreateLogger(); }
(4)具體工廠:FileLoggerFactory和DatabaseLoggerFactory類
public class FileLoggerFactory : ILoggerFactory { public ILogger CreateLogger() { // 創建文件日志記錄器 ILogger logger = new FileLogger(); // 創建文件,代碼省略 return logger; } } public class DatabaseLoggerFactory : ILoggerFactory { public ILogger CreateLogger() { // 連接數據庫,代碼省略 // 創建數據庫日志記錄器對象 ILogger logger = new DatabaseLogger(); // 初始化數據庫日志記錄器,代碼省略 return logger; } }
(5)客戶端調用
public static void Main() { ILoggerFactory factory = new FileLoggerFactory(); // 可通過引入配置文件實現 if (factory == null) { return; } ILogger logger = factory.CreateLogger(); logger.WriteLog(); }
運行結果如下圖:

四、借助反射的重構版本
4.1 逃離修改客戶端的折磨
為了讓系統具有更好的靈活性和可擴展性,M公司程序猿決定對日志記錄器客戶端代碼進行重構,使得可以在不修改任何客戶端代碼的基礎之上更換或是增加新的日志記錄方式。
在客戶端代碼中將不再使用new關鍵字來創建工廠對象,而是將具體工廠類的類名存在配置文件(例如XML文件)中,通過讀取配置文件來獲取類名,再借助.NET反射機制來動態地創建對象實例。
4.2 擼起袖子開始重構
(1)創建配置文件
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="LoggerFactory" value="Manulife.ChengDu.DesignPattern.FactoryMethod.v2.DatabaseLoggerFactory, Manulife.ChengDu.DesignPattern.FactoryMethod" /> </appSettings> </configuration>
(2)封裝一個簡單的AppConfigHelper類
public class AppConfigHelper { public static string GetLoggerFactoryName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["LoggerFactory"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetLoggerFactoryInstance() { string assemblyName = AppConfigHelper.GetLoggerFactoryName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
(2)重構客戶端代碼
public static void Main() { ILoggerFactory factory = (ILoggerFactory)AppConfigHelper.GetLoggerFactoryInstance(); if (factory == null) { return; } ILogger logger = factory.CreateLogger(); logger.WriteLog(); }
運行結果如下圖所示:

五、工廠方法的隱藏
有時候,為了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法,客戶端無須調用工廠方法創建產品,直接通過工廠即可使用所創建的對象中的業務方法。
(1)修改抽象工廠
public abstract class LoggerFactory { // 在工廠類中直接調用日志記錄器的業務方法WriteLog() public void WriteLog() { ILogger logger = this.CreateLogger(); logger.WriteLog(); } public abstract ILogger CreateLogger(); }
(2)修改具體工廠
public class DatabaseLoggerFactory : LoggerFactory { public override ILogger CreateLogger() { // 連接數據庫,代碼省略 // 創建數據庫日志記錄器對象 ILogger logger = new DatabaseLogger(); // 初始化數據庫日志記錄器,代碼省略 return logger; } }
(3)簡化的客戶端調用
public static void Main() { LoggerFactory factory = (LoggerFactory)AppConfigHelper.GetLoggerFactoryInstance(); if (factory == null) { return; } factory.WriteLog(); }
六、工廠方法模式總結
5.1 主要優點
- 工廠方法用于創建客戶所需要的產品,還向客戶隱藏了哪種具體產品類將被實例化這一細節。因此,用戶只需要關心所需產品對應的工廠,無須關心創建細節。
- 在系統中加入新產品時,無需修改抽象工廠和抽象產品提供的接口,也無須修改客戶端,還無須修改其他的具體工廠和具體產品,而只要加入一個具體工廠和具體產品就可以了。因此,系統的可擴展性得到了保證,符合開閉原則。
5.2 主要缺點
- 在添加新產品時,需要編寫新的具體產品類,還要提供與之對應的具體工廠類,系統中類的個數將成對增加,一定程度上增加了系統的復雜度。
- 由于考慮到系統的可擴展性,需要引入抽象層,且在實現時可能需要用到反射等技術,增加了系統的實現難度。
5.3 適用場景
- 客戶端不知道其所需要的對象的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的的工廠即可,具體的產品對象由具體工廠創建,可將具體工廠的類名存儲到配置文件或數據庫中。
- 抽象工廠類通過其子類來指定創建哪個對象。在工廠方法模式中,抽象工廠類只需要提供一個創建產品的接口,而由其子類來確定具體要創建的對象,利用面向對象的多態性和里氏替換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統易于擴展。
參考資料

劉偉,《設計模式的藝術—軟件開發人員內功修煉之道》

上一篇的簡單工廠模式雖然簡單,但是存在一個很嚴重的問題:當系統中需要引入新產品時,由于靜態工廠方法通過所傳入參數的不同來創建不同的產品,這必定要修改工廠類的源代碼,將違背開閉原則。如何實現新增新產品而不影響已有代碼?工廠方法模式為此應運而生。工廠方法用于創建客戶所需要的產品,還向客戶隱藏了哪種具體產品類將被實例化這一細節。因此,用戶只需要關心所需產品對應的工廠,無須關心創建細節。

浙公網安備 33010602011771號