概述
在軟件系統中,經常面臨著“某個對象”的創建工作,由於需求的變化,這個對象的具體實現經常面臨著劇烈的變化,但是它卻擁有比較穩定的接口。如何應對這種變化?提供一種封裝機制來隔離出“這個易變對象”的變化,從而保持系統中“其它依賴該對象的對象”不隨著需求的改變而改變?這就是要說的Factory Method模式了。
意圖
定義一個用戶創建對象的接口,讓子類決定實例化哪一個類。Factory Method使一個類的實例化延遲到其子類。
結構圖
生活中的例子
工廠方法定義一個用於創建對象的接口,但是讓子類決定實例化哪個類。壓注成型演示了這種模式。塑料玩具制造商加工塑料粉,將塑料注入到希望形狀的模具中。玩具的類別(車,人物等等)是由模具決定的。
工廠方法解說
在工廠方法模式中,核心的工廠類不再負責所有產品的創建,而是將具體創建工作交給子類去做。這個核心類僅僅負責給出具體工廠必須實現的接口,而不接觸哪一個產品類被實例化這種細節。這使得工廠方法模式可以允許系統在不修改工廠角色的情況下引進新產品。在Factory Method模式中,工廠類與產品類往往具有平行的等級結構,它們之間一一對應。
現在我們考慮一個日志記錄的例子(這裡我們只是為了說明Factory Method模式,實際項目中的日志記錄不會這麼去做,也要比這復雜一些)。假定我們要設計日志記錄的類,支持記錄的方法有FileLog和EventLog兩種方式。在這裡我們先不談設計模式,那麼這個日志記錄的類就很好實現了:
1/**//// <summary>
2/// 日志記錄類
3/// </summary>
4public class Log
5 {
6
7 public void WriteEvent()
8 {
9 Console.WriteLine("EventLog Success!");
10 }
11
12 public void WriteFile()
13 {
14 Console.WriteLine("FileLog Success!");
15 }
16
17 public void Write(string LogType)
18 {
19 switch(LogType.ToLower())
20 {
21 case "event":
22 WriteEvent();
23 break;
24
25 case "file":
26 WriteFile();
27 break;
28
29 default:
30 break;
31 }
32 }
33 }
34
這樣的程序結構顯然不能符合我們的要求,如果我們增加一種新的日志記錄的方式DatabaseLog,那就要修改Log類,隨著記錄方式的變化,switch語句在不斷的變化,這樣就引起了整個應用程序的不穩定,進一步分析上面的代碼,發現對於EventLog和FileLog是兩種完全不同的記錄方式,它們之間不應該存在必然的聯系,而應該把它們分別作為單獨的對象來對待。
1/**//// <summary>
2/// EventLog類
3/// </summary>
4public class EventLog
5{
6 public void Write()
7 {
8 Console.WriteLine("EventLog Write Success!");
9 }
10}
11
12/**//// <summary>
13/// FileLog類
14/// </summary>
15public class FileLog
16{
17 public void Write()
18 {
19 Console.WriteLine("FileLog Write Success!");
20 }
21}
22
進一步抽象,為它們抽象出一個共同的父類,結構圖如下:
實現代碼:
1/**//// <summary>
2/// Log類
3/// </summary>
4public abstract class Log
5{
6 public abstract void Write();
7}
8
此時EventLog和FileLog類的代碼應該如下:
1/**//// <summary>
2/// EventLog類
3/// </summary>
4public class EventLog:Log
5{
6 public override void Write()
7 {
8 Console.WriteLine("EventLog Write Success!");
9 }
10}
11/**//// <summary>
12/// FileLog類
13/// </summary>
14public class FileLog:Log
15{
16 public override void Write()
17 {
18 Console.WriteLine("FileLog Write Success!");
19 }
20}
21
此時我們再看增加新的記錄日志方式DatabaseLog的時候,需要做哪些事情?只需要增加一個繼承父類Log的子類來實現,而無需再去修改EventLog和FileLog類,這樣的設計滿足了類之間的層次關系,又很好的符合了面向對象設計中的單一職責原則,每一個類都只負責一件具體的事情。到這裡似乎我們的設計很完美了,事實上我們還沒有看客戶程序如何去調用。 在應用程序中,我們要使用某一種日志記錄方式,也許會用到如下這樣的語句:
EventLog eventlog = new EventLog();
eventlog.Write();
當日志記錄的方式從EventLog變化為FileLog,我們就得修改所有程序代碼中出現上面語句的部分,這樣的工作量是可想而知的。此時就需要解耦具體的日志記錄方式和應用程序。這就要引入Factory Method模式了,每一個日志記錄的對象就是工廠所生成的產品,既然有兩種記錄方式,那就需要兩個不同的工廠去生產了,代碼如下:
1/**//// <summary>
2/// EventFactory類
3/// </summary>
4public class EventFactory
5{
6 public EventLog Create()
7 {
8 return new EventLog();
9 }
10}
11/**//// <summary>
12/// FileFactory類
13/// </summary>
14public class FileFactory
15{
16 public FileLog Create()
17 {
18 return new FileLog();
19 }
20}
21
這兩個工廠和具體的產品之間是平行的結構,並一一對應,並在它們的基礎上抽象出一個公用的接口,結構圖如下:
實現代碼如下:
1/**//// <summary>
2/// LogFactory類
3/// </summary>
4public abstract class LogFactory
5{
6 public abstract Log Create();
7}
8
此時兩個具體工廠的代碼應該如下:
1/**//// <summary>
2/// EventFactory類
3/// </summary>
4public class EventFactory:LogFactory
5{
6 public override EventLog Create()
7 {
8 return new EventLog();
9 }
10}
11/**//// <summary>
12/// FileFactory類
13/// </summary>
14public class FileFactory:LogFactory
15{
16 public override FileLog Create()
17 {
18 return new FileLog();
19 }
20}
21
這樣通過工廠方法模式我們把上面那對象創建工作封裝在了工廠中,此時我們似乎完成了整個Factory Method的過程。這樣達到了我們應用程序和具體日志記錄對象之間解耦的目的了嗎?看一下此時客戶端程序代碼:
1/**//// <summary>
2/// App類
3/// </summary>
4public class App
5{
6 public static void Main(string[] args)
7 {
8 LogFactory factory = new EventFactory();
9
10 Log log = factory.Create();
11
12 log.Write();
13 }
14}
15
在客戶程序中,我們有效地避免了具體產品對象和應用程序之間的耦合,可是我們也看到,增加了具體工廠對象和應用程序之間的耦合。那這樣究竟帶來什麼好處呢?我們知道,在應用程序中,Log對象的創建是頻繁的,在這裡我們可以把
LogFactory factory = new EventFactory();
這句話放在一個類模塊中,任何需要用到Log對象的地方仍然不變。要是換一種日志記錄方式,只要修改一處為:
LogFactory factory = new FileFactory();
其余的任何地方我們都不需要去修改。有人會說那還是修改代碼,其實在開發中我們很難避免修改,但是我們可以盡量做到只修改一處。
其實利用.NET的特性,我們可以避免這種不必要的修改。下面我們利用.NET中的反射機制來進一步修改我們的程序,這時就要用到配置文件了,如果我們想使用哪一種日志記錄方式,則在相應的配置文件中設置如下:
1<appSettings>
2 <add key="factoryName" value="EventFactory"></add>
3</appSettings>
4
此時客戶端代碼如下:
1/**//// <summary>
2/// App類
3/// </summary>
4public class App
5{
6 public static void Main(string[] args)
7 {
8 string strfactoryName = ConfigurationSettings.AppSettings["factoryName"];
9
10 LogFactory factory;
11 factory = (LogFactory)Assembly.Load("FactoryMethod").CreateInstance("FactoryMethod." + strfactoryName);
12
13 Log log = factory.Create();
14 log.Write();
15 }
16}
17
現在我們看到,在引進新產品(日志記錄方式)的情況下,我們並不需要去修改工廠類,而只是增加新的產品類和新的工廠類(注意:這是任何時候都不能避免的),這樣很好的符合了開放封閉原則。
ASP.NET HTTP通道中的應用
Factory Method模式在ASP.NET HTTP通道中我們可以找到很多的例子。ASP.NET HTTP通道是System.Web命名空間下的一個類,WEB Server使用該類處理接收到的HTTP請求,並給客戶端發送響應。HTTP通道主要的工作有Session管理,應用程序池管理,緩存管理,安全等。
System.Web.HttpApplicationFactory
HttpRuntime是HTTP通道的入口點,它根據每一個具體的請求創建一個HttpContext實例, HttpRuntime並沒有確定它將要處理請求的HttpApplication對象的類型,它調用了一個靜態的工廠方法HttpApplicationFactory.GetApplicationInstance,通過它來創建HttpContext實例。GetApplicationInstance使用HttpContext實例來確定針對這個請求該響應哪個虛擬路徑,如果這個虛擬路徑以前請求過,HttpApplication(或者一個繼承於ASP.Global_asax的類的實例)將直接從應用程序池中返回,否則針對該虛擬路徑將創建一個新的HttpApplication對象並返回。如下圖所示:
HttpApplicationFactory.GetApplicationInstance帶有一個類型為HttpContext的參數,創建的所有對象(產品)都是HttpApplication的類型,通過反編譯,來看一下GetApplicationInstance的實現:
1internal static IHttpHandler GetApplicationInstance(HttpContext context)
2{
3 if (HttpApplicationFactory._customApplication != null)
4 {
5 return HttpApplicationFactory._customApplication;
6 }
7 if (HttpDebugHandler.IsDebuggingRequest(context))
8 {
9 return new HttpDebugHandler();
10 }
11 if (!HttpApplicationFactory._theApplicationFactory._inited)
12 {
13 lock (HttpApplicationFactory._theApplicationFactory)
14 {
15 if (!HttpApplicationFactory._theApplicationFactory._inited)
16 {
17 HttpApplicationFactory._theApplicationFactory.Init(context);
18 HttpApplicationFactory._theApplicationFactory._inited = true;
19 }
20 }
21 }
22 return HttpApplicationFactory._theApplicationFactory.GetNormalApplicationInstance(context);
23}
24
System.Web.IHttpHandlerFactory
我們來做進一步的探索,HttpApplication實例需要一個Handler對象來處理資源請求, HttpApplication的主要任務就是找到真正處理請求的類。HttpApplication首先確定了一個創建Handler對象的工廠,來看一下在Machine.config文件中的配置區<httphandlers>,在配置文件注冊了應用程序的具體處理類。例如在Machine.config中對*.aspx的處理將映射到System.Web.UI.PageHandlerFactory 類,而對*.ashx的處理將映射到System.Web.UI.SimpleHandlerFactory 類,這兩個類都是繼承於IhttpHandlerFactory接口的具體類:
<httpHandlers>
<add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory" />
<add verb="*" path="*.ashx" type="System.Web.UI.SimpleHandlerFactory" />
</httpHandlers>
這個配置區建立了資源請求的類型和處理請求的類之間的一個映射集。如果一個.aspx頁面發出了請求,將會調用System.Web.UI.PageHandlerFactory類,HttpApplication調用接口IHttpHandlerFactory中的工廠方法GetHandler來創建一個Handler對象。當一個名為sample.aspx的頁面發出請求時,通過PageHandlerFactory將返回一個ASP.SamplePage_aspx對象(具體產品),如下圖:
IHttpHandlerFactory工廠:
1public interface IHttpHandlerFactory
2{
3 // Methods
4 IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated);
5 void ReleaseHandler(IHttpHandler handler);
6}
7
IHttpHandlerFactory.GetHandler是一個工廠方法模式的典型例子,在這個應用中,各個角色的設置如下:
抽象工廠角色:IHttpHandlerFactory
具體工廠角色:PageHandlerFactory
抽象產品角色:IHttpHandler
具體產品角色:ASP.SamplePage_aspx
進一步去理解
理解上面所說的之後,我們就可以去自定義工廠類來對特定的資源類型進行處理。第一步我們需要創建兩個類去分別實現IHttpHandlerFactory 和IHttpHandler這兩個接口。
1public class HttpHandlerFactoryImpl:IHttpHandlerFactory {
2
3 IHttpHandler IHttpHandlerFactory.GetHandler(
4 HttpContext context, String requestType,
5 String url, String pathTranslated ) {
6
7 return new HttpHandlerImpl();
8
9 }//IHttpHandlerFactory.GetHandler
10
11 void IHttpHandlerFactory.ReleaseHandler(
12 IHttpHandler handler) { /**//*no-op*/ }
13
14}//HttpHandlerFactoryImpl
15
16public class HttpHandlerImpl:IHttpHandler {
17
18 void IHttpHandler.ProcessRequest(HttpContext context) {
19
20 context.Response.Write("sample handler invoked");
21
22 }//ProcessRequest
23
24 bool IHttpHandler.IsReusable { get { return false; } }
25
26}//HttpHandlerImpl
27
第二步需要在配置文件中建立資源請求類型和處理程序之間的映射。我們希望當請求的類型為*.sample時進入我們自定義的處理程序,如下:
<httpHandlers>
<add verb="*" path="*.sample"
type="HttpHandlerFactoryImpl,SampleHandler" />
</httpHandlers>
最後一步我們需要把文件擴展*.sample映射到ASP.NET ISAPI擴展DLL(aspnet_isapi.dll)上。由於我們已經建立了用於處理新擴展文件的處理程序了,我們還需要把這個擴展名告訴IIS並把它映射到ASP.NET。如果你不執行這個步驟而試圖訪問*.sample文件,IIS將簡單地返回該文件而不是把它傳遞給ASP.NET運行時。其結果是該HTTP處理程序不會被調用。
運行Internet服務管理器,右鍵點擊默認Web站點,選擇屬性,移動到主目錄選項頁,並點擊配置按鈕。應用程序配置對話框彈出來了。點擊添加按鈕並在可執行字段輸入aspnet_isapi.dll文件路徑,在擴展字段輸入.sample。其它字段不用處理;該對話框如下所示:
在.NET Framework中,關於工廠模式的使用有很多的例子,例如IEnumerable和IEnumerator就是一個Creator和一個Product;System.Security.Cryptography中關於加密算法的選擇,SymmetricAlgorithm, AsymmetricAlgorithm, 和HashAlgorithm分別是三個工廠,他們各有一個靜態的工廠方法Create;System.Net.WebRequest是 .NET Framework 的用於訪問 Internet 數據的請求/響應模型的抽象基類。使用該請求/響應模型的應用程序可以用協議不可知的方式從 Internet 請求數據。在這種方式下,應用程序處理 WebRequest 類的實例,而協議特定的子類則執行請求的具體細節。請求從應用程序發送到某個特定的 URI,如服務器上的 Web 頁。URI 從一個為應用程序注冊的 WebRequest 子代列表中確定要創建的適當子類。注冊 WebRequest 子代通常是為了處理某個特定的協議(如 HTTP 或 FTP),但是也可以注冊它以處理對特定服務器或服務器上的路徑的請求。有時間我會就.NET Framework中工廠模式的使用作一個專題總結。
實現要點
1.Factory Method模式的兩種情況:一是Creator類是一個抽象類且它不提供它所聲明的工廠方法的實現;二是Creator是一個具體的類且它提供一個工廠方法的缺省實現。
2.工廠方法是可以帶參數的。
3.工廠的作用並不僅僅只是創建一個對象,它還可以做對象的初始化,參數的設置等。
效果
1.用工廠方法在一個類的內部創建對象通常比直接創建對象更靈活。
2.Factory Method模式通過面向對象的手法,將所要創建的具體對象的創建工作延遲到了子類,從而提供了一種擴展的策略,較好的解決了這種緊耦合的關系。
適用性
在以下情況下,適用於工廠方法模式:
1.當一個類不知道它所必須創建的對象的類的時候。
2.當一個類希望由它的子類來指定它所創建的對象的時候。
3.當類將創建對象的職責委托給多個幫助子類中的某一個,並且你希望將哪一個幫助子類是代理者這一信息局部化的時候。
總結
Factory Method模式是設計模式中應用最為廣泛的模式,通過本文,相信讀者已經對它有了一定的認識。然而我們要明確的是:在面向對象的編程中,對象的創建工作非常簡單,對象的創建時機卻很重要。Factory Method要解決的就是對象的創建時機問題,它提供了一種擴展的策略,很好地符合了開放封閉原則。