簡單工廠模式雖然簡單,但存在一個很嚴重的問題。當系統中需要引入新產品時,由于靜態工廠方法通過所傳入參數的不同來創建不同的產品,這必定要修改工廠類的源代碼,將違背“開閉原則”,如何實現增加新產品而不影響已有代碼?工廠方法模式應運而生,本文將介紹第二種工廠模式——工廠方法模式。
1日志記錄器的設計
Sunny軟件公司欲開發一個系統運行日志記錄器(Logger),該記錄器可以通過多種途徑保存系統的運行日志,如通過文件記錄或數據庫記錄,用戶可以通過修改配置文件靈活地更換日志記錄方式。在設計各類日志記錄器時,Sunny公司的開發人員發現需要對日志記錄器進行一些初始化工作,初始化參數的設置過程較為復雜,而且某些參數的設置有嚴格的先后次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程并保證多種記錄器切換的靈活性是Sunny公司開發人員面臨的一個難題。
Sunny公司的開發人員通過對該需求進行分析,發現該日志記錄器有兩個設計要點:
(1)需要封裝日志記錄器的初始化過程,這些初始化工作較為復雜,例如需要初始化其他相關的類,還有可能需要讀取配置文件(例如連接數據庫或創建文件),導致代碼較長,如果將它們都寫在構造函數中,會導致構造函數龐大,不利于代碼的修改和維護;
(2)用戶可能需要更換日志記錄方式,在客戶端代碼中需要提供一種靈活的方式來選擇日志記錄器,盡量在不修改源代碼的基礎上更換或者增加日志記錄方式。
Sunny公司開發人員最初使用簡單工廠模式對日志記錄器進行了設計,初始結構如圖1所示:
圖1基于簡單工廠模式設計的日志記錄器結構圖
在圖1中,LoggerFactory充當創建日志記錄器的工廠,提供了工廠方法createLogger()用于創建日志記錄器,Logger是抽象日志記錄器接口,其子類為具體日志記錄器。其中,工廠類LoggerFactory代碼片段如下所示:
[java]view plaincopy
//日志記錄器工廠
classLoggerFactory?{
//靜態工廠方法
publicstaticLogger?createLogger(String?args)?{
if(args.equalsIgnoreCase("db"))?{
//連接數據庫,代碼省略
//創建數據庫日志記錄器對象
Logger?logger?=newDatabaseLogger();
//初始化數據庫日志記錄器,代碼省略
returnlogger;
}
elseif(args.equalsIgnoreCase("file"))?{
//創建日志文件
//創建文件日志記錄器對象
Logger?logger?=newFileLogger();
//初始化文件日志記錄器,代碼省略
returnlogger;
}
else{
returnnull;
}
}
}
為了突出設計重點,我們對上述代碼進行了簡化,省略了具體日志記錄器類的初始化代碼。在LoggerFactory類中提供了靜態工廠方法createLogger(),用于根據所傳入的參數創建各種不同類型的日志記錄器。通過使用簡單工廠模式,我們將日志記錄器對象的創建和使用分離,客戶端只需使用由工廠類創建的日志記錄器對象即可,無須關心對象的創建過程,但是我們發現,雖然簡單工廠模式實現了對象的創建和使用分離,但是仍然存在如下兩個問題:
(1)工廠類過于龐大,包含了大量的if…else…代碼,導致維護和測試難度增大;
(2)系統擴展不靈活,如果增加新類型的日志記錄器,必須修改靜態工廠方法的業務邏輯,違反了“開閉原則”。
如何解決這兩個問題,提供一種簡單工廠模式的改進方案?這就是本文所介紹的工廠方法模式的動機之一。
2 工廠方法模式概述
在簡單工廠模式中只提供一個工廠類,該工廠類處于對產品類進行實例化的中心位置,它需要知道每一個產品對象的創建細節,并決定何時實例化哪一個產品類。簡單工廠模式最大的缺點是當有新產品要加入到系統中時,必須修改工廠類,需要在其中加入必要的業務邏輯,這違背了“開閉原則”。此外,在簡單工廠模式中,所有的產品都由同一個工廠創建,工廠類職責較重,業務邏輯較為復雜,具體產品與工廠類之間的耦合度高,嚴重影響了系統的靈活性和擴展性,而工廠方法模式則可以很好地解決這一問題。
在工廠方法模式中,我們不再提供一個統一的工廠類來創建所有的產品對象,而是針對不同的產品提供不同的工廠,系統提供一個與產品等級結構對應的工廠等級結構。工廠方法模式定義如下:
工廠方法模式(Factory Method Pattern):定義一個用于創建對象的接口,讓子類決定將哪一個類實例化。工廠方法模式讓一個類的實例化延遲到其子類。工廠方法模式又簡稱為工廠模式(Factory Pattern),又可稱作虛擬構造器模式(Virtual Constructor Pattern)或多態工廠模式(Polymorphic Factory Pattern)。工廠方法模式是一種類創建型模式。
工廠方法模式提供一個抽象工廠接口來聲明抽象工廠方法,而由其子類來具體實現工廠方法,創建具體的產品對象。工廠方法模式結構如圖2所示:
圖2工廠方法模式結構圖
在工廠方法模式結構圖中包含如下幾個角色:
●Product(抽象產品):它是定義產品的接口,是工廠方法模式所創建對象的超類型,也就是產品對象的公共父類。
●ConcreteProduct(具體產品):它實現了抽象產品接口,某種類型的具體產品由專門的具體工廠創建,具體工廠和具體產品之間一一對應。
●Factory(抽象工廠):在抽象工廠類中,聲明了工廠方法(Factory Method),用于返回一個產品。抽象工廠是工廠方法模式的核心,所有創建對象的工廠類都必須實現該接口。
●ConcreteFactory(具體工廠):它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,并可由客戶端調用,返回一個具體產品類的實例。
與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠可以是接口,也可以是抽象類或者具體類,其典型代碼如下所示:
[java]view plaincopy
interfaceFactory?{
publicProduct?factoryMethod();
}
在抽象工廠中聲明了工廠方法但并未實現工廠方法,具體產品對象的創建由其子類負責,客戶端針對抽象工廠編程,可在運行時再指定具體工廠類,具體工廠類實現了工廠方法,不同的具體工廠可以創建不同的具體產品,其典型代碼如下所示:
[java]view plaincopy
classConcreteFactoryimplementsFactory?{
publicProduct?factoryMethod()?{
returnnewConcreteProduct();
}
}
在實際使用時,具體工廠類在實現工廠方法時除了創建具體產品對象之外,還可以負責產品對象的初始化工作以及一些資源和環境配置工作,例如連接數據庫、創建文件等。
在客戶端代碼中,只需關心工廠類即可,不同的具體工廠可以創建不同的產品,典型的客戶端類代碼片段如下所示:
[java]view plaincopy
……
Factory?factory;
factory?=newConcreteFactory();//可通過配置文件實現
Product?product;
product?=?factory.factoryMethod();
……
可以通過配置文件來存儲具體工廠類ConcreteFactory的類名,更換新的具體工廠時無須修改源代碼,系統擴展更為方便。
3完整解決方案
Sunny公司開發人員決定使用工廠方法模式來設計日志記錄器,其基本結構如圖3所示:
圖3日志記錄器結構圖
在圖3中,Logger接口充當抽象產品,其子類FileLogger和DatabaseLogger充當具體產品,LoggerFactory接口充當抽象工廠,其子類FileLoggerFactory和DatabaseLoggerFactory充當具體工廠。完整代碼如下所示:
[java]view plaincopy
//日志記錄器接口:抽象產品
interfaceLogger?{
publicvoidwriteLog();
}
//數據庫日志記錄器:具體產品
classDatabaseLoggerimplementsLogger?{
publicvoidwriteLog()?{
System.out.println("數據庫日志記錄。");
}
}
//文件日志記錄器:具體產品
classFileLoggerimplementsLogger?{
publicvoidwriteLog()?{
System.out.println("文件日志記錄。");
}
}
//日志記錄器工廠接口:抽象工廠
interfaceLoggerFactory?{
publicLogger?createLogger();
}
//數據庫日志記錄器工廠類:具體工廠
classDatabaseLoggerFactoryimplementsLoggerFactory?{
publicLogger?createLogger()?{
//連接數據庫,代碼省略
//創建數據庫日志記錄器對象
Logger?logger?=newDatabaseLogger();
//初始化數據庫日志記錄器,代碼省略
returnlogger;
}
}
//文件日志記錄器工廠類:具體工廠
classFileLoggerFactoryimplementsLoggerFactory?{
publicLogger?createLogger()?{
//創建文件日志記錄器對象
Logger?logger?=newFileLogger();
//創建文件,代碼省略
returnlogger;
}
}
編寫如下客戶端測試代碼:
[java]view plaincopy
classClient?{
publicstaticvoidmain(String?args[])?{
LoggerFactory?factory;
Logger?logger;
factory?=newFileLoggerFactory();//可引入配置文件實現
logger?=?factory.createLogger();
logger.writeLog();
}
}
編譯并運行程序,輸出結果如下:
文件日志記錄。
4 反射與配置文件
為了讓系統具有更好的靈活性和可擴展性,Sunny公司開發人員決定對日志記錄器客戶端代碼進行重構,使得可以在不修改任何客戶端代碼的基礎上更換或增加新的日志記錄方式。
在客戶端代碼中將不再使用new關鍵字來創建工廠對象,而是將具體工廠類的類名存儲在配置文件(如XML文件)中,通過讀取配置文件獲取類名字符串,再使用Java的反射機制,根據類名字符串生成對象。在整個實現過程中需要用到兩個技術:Java反射機制與配置文件讀取。軟件系統的配置文件通常為XML文件,我們可以使用DOM (Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技術來處理XML文件。關于DOM、SAX、StAX等技術的詳細學習大家可以參考其他相關資料,在此不予擴展。
擴展
關于Java與XML的相關資料,大家可以閱讀Tom Myers和Alexander Nakhimovsky所著的《JavaXML編程指南》一書或訪問developer Works中國中的“javaXML技術專題”,參考鏈接:
http://www.ibm.com/developerworks/cn/xml/theme/x-java.html
Java反射(Java Reflection)是指在程序運行時獲取已知名稱的類或已有對象的相關信息的一種機制,包括類的方法、屬性、父類等信息,還包括實例的創建和實例類型的判斷等。在反射中使用最多的類是Class,Class類的實例表示正在運行的Java應用程序中的類和接口,其forName(String className)方法可以返回與帶有給定字符串名的類或接口相關聯的Class對象,再通過Class對象的newInstance()方法創建此對象所表示的類的一個新實例,即通過一個類名字符串得到類的實例。如創建一個字符串類型的對象,其代碼如下:
[java]view plaincopy
//通過類名生成實例對象并將其返回
Class?c=Class.forName("String");
Object?obj=c.newInstance();
returnobj;
此外,在JDK中還提供了java.lang.reflect包,封裝了其他與反射相關的類,此處只用到上述簡單的反射代碼,在此不予擴展。
Sunny公司開發人員創建了如下XML格式的配置文件config.xml用于存儲具體日志記錄器工廠類類名:
[html]view plaincopy
FileLoggerFactory
為了讀取該配置文件并通過存儲在其中的類名字符串反射生成對象,Sunny公司開發人員開發了一個名為XMLUtil的工具類,其詳細代碼如下所示:
[java]view plaincopy
//工具類XMLUtil.java
importjavax.xml.parsers.*;
importorg.w3c.dom.*;
importorg.xml.sax.SAXException;
importjava.io.*;
publicclassXMLUtil?{
//該方法用于從XML配置文件中提取具體類類名,并返回一個實例對象
publicstaticObject?getBean()?{
try{
//創建DOM文檔對象
DocumentBuilderFactory?dFactory?=?DocumentBuilderFactory.newInstance();
DocumentBuilder?builder?=?dFactory.newDocumentBuilder();
Document?doc;
doc?=?builder.parse(newFile("config.xml"));
//獲取包含類名的文本節點
NodeList?nl?=?doc.getElementsByTagName("className");
Node?classNode=nl.item(0).getFirstChild();
String?cName=classNode.getNodeValue();
//通過類名生成實例對象并將其返回
Class?c=Class.forName(cName);
Object?obj=c.newInstance();
returnobj;
}
catch(Exception?e)?{
e.printStackTrace();
returnnull;
}
}
}
有了XMLUtil類后,可以對日志記錄器的客戶端代碼進行修改,不再直接使用new關鍵字來創建具體的工廠類,而是將具體工廠類的類名存儲在XML文件中,再通過XMLUtil類的靜態工廠方法getBean()方法進行對象的實例化,代碼修改如下:
[java]view plaincopy
classClient?{
publicstaticvoidmain(String?args[])?{
LoggerFactory?factory;
Logger?logger;
factory?=?(LoggerFactory)XMLUtil.getBean();//getBean()的返回類型為Object,需要進行強制類型轉換
logger?=?factory.createLogger();
logger.writeLog();
}
}
引入XMLUtil類和XML配置文件后,如果要增加新的日志記錄方式,只需要執行如下幾個步驟:
(1)新的日志記錄器需要繼承抽象日志記錄器Logger;
(2)對應增加一個新的具體日志記錄器工廠,繼承抽象日志記錄器工廠LoggerFactory,并實現其中的工廠方法createLogger(),設置好初始化參數和環境變量,返回具體日志記錄器對象;
(3)修改配置文件config.xml,將新增的具體日志記錄器工廠類的類名字符串替換原有工廠類類名字符串;
(4)編譯新增的具體日志記錄器類和具體日志記錄器工廠類,運行客戶端測試類即可使用新的日志記錄方式,而原有類庫代碼無須做任何修改,完全符合“開閉原則”。
通過上述重構可以使得系統更加靈活,由于很多設計模式都關注系統的可擴展性和靈活性,因此都定義了抽象層,在抽象層中聲明業務方法,而將業務方法的實現放在實現層中。
5重載的工廠方法
Sunny公司開發人員通過進一步分析,發現可以通過多種方式來初始化日志記錄器,例如可以為各種日志記錄器提供默認實現;還可以為數據庫日志記錄器提供數據庫連接字符串,為文件日志記錄器提供文件路徑;也可以將參數封裝在一個Object類型的對象中,通過Object對象將配置參數傳入工廠類。此時,可以提供一組重載的工廠方法,以不同的方式對產品對象進行創建。當然,對于同一個具體工廠而言,無論使用哪個工廠方法,創建的產品類型均要相同。如圖4所示:
圖4重載的工廠方法結構圖
引入重載方法后,抽象工廠LoggerFactory的代碼修改如下:
[java]view plaincopy
interfaceLoggerFactory?{
publicLogger?createLogger();
publicLogger?createLogger(String?args);
publicLogger?createLogger(Object?obj);
}
具體工廠類DatabaseLoggerFactory代碼修改如下:
[java]view plaincopy
classDatabaseLoggerFactoryimplementsLoggerFactory?{
publicLogger?createLogger()?{
//使用默認方式連接數據庫,代碼省略
Logger?logger?=newDatabaseLogger();
//初始化數據庫日志記錄器,代碼省略
returnlogger;
}
publicLogger?createLogger(String?args)?{
//使用參數args作為連接字符串來連接數據庫,代碼省略
Logger?logger?=newDatabaseLogger();
//初始化數據庫日志記錄器,代碼省略
returnlogger;
}
publicLogger?createLogger(Object?obj)?{
//使用封裝在參數obj中的連接字符串來連接數據庫,代碼省略
Logger?logger?=newDatabaseLogger();
//使用封裝在參數obj中的數據來初始化數據庫日志記錄器,代碼省略
returnlogger;
}
}
//其他具體工廠類代碼省略
在抽象工廠中定義多個重載的工廠方法,在具體工廠中實現了這些工廠方法,這些方法可以包含不同的業務邏輯,以滿足對不同產品對象的需求。
6工廠方法的隱藏
有時候,為了進一步簡化客戶端的使用,還可以對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法,客戶端無須調用工廠方法創建產品,直接通過工廠即可使用所創建的對象中的業務方法。
如果對客戶端隱藏工廠方法,日志記錄器的結構圖將修改為圖5所示:
圖5隱藏工廠方法后的日志記錄器結構圖
在圖5中,抽象工廠類LoggerFactory的代碼修改如下:
[java]view plaincopy
//改為抽象類
abstractclassLoggerFactory?{
//在工廠類中直接調用日志記錄器類的業務方法writeLog()
publicvoidwriteLog()?{
Logger?logger?=this.createLogger();
logger.writeLog();
}
publicabstractLogger?createLogger();
}
客戶端代碼修改如下:
[java]view plaincopy
classClient?{
publicstaticvoidmain(String?args[])?{
LoggerFactory?factory;
factory?=?(LoggerFactory)XMLUtil.getBean();
factory.writeLog();//直接使用工廠對象來調用產品對象的業務方法
}
}
通過將業務方法的調用移入工廠類,可以直接使用工廠對象來調用產品對象的業務方法,客戶端無須直接使用工廠方法,在某些情況下我們也可以使用這種設計方案。
工廠方法模式是簡單工廠模式的延伸,它繼承了簡單工廠模式的優點,同時還彌補了簡單工廠模式的不足。工廠方法模式是使用頻率最高的設計模式之一,是很多開源框架和API類庫的核心模式。
1.主要優點
工廠方法模式的主要優點如下:
(1)在工廠方法模式中,工廠方法用來創建客戶所需要的產品,同時還向客戶隱藏了哪種具體產品類將被實例化這一細節,用戶只需要關心所需產品對應的工廠,無須關心創建細節,甚至無須知道具體產品類的類名。
(2)基于工廠角色和產品角色的多態性設計是工廠方法模式的關鍵。它能夠讓工廠可以自主確定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多態工廠模式,就正是因為所有的具體工廠類都具有同一抽象父類。
(3)使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的接口,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要添加一個具體工廠和具體產品就可以了,這樣,系統的可擴展性也就變得非常好,完全符合“開閉原則”。
2.主要缺點
工廠方法模式的主要缺點如下:
(1)在添加新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的復雜度,有更多的類需要編譯和運行,會給系統帶來一些額外的開銷。
(2)由于考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時可能需要用到DOM、反射等技術,增加了系統的實現難度。
3.適用場景
在以下情況下可以考慮使用工廠方法模式:
(1)客戶端不知道它所需要的對象的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品對象由具體工廠類創建,可將具體工廠類的類名存儲在配置文件或數據庫中。
(2)抽象工廠類通過其子類來指定創建哪個對象。在工廠方法模式中,對于抽象工廠類只需要提供一個創建產品的接口,而由其子類來確定具體要創建的對象,利用面向對象的多態性和里氏代換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。