1 日誌記錄器的設計
Sunny軟件公司欲開發一個系統運行日誌記錄器(Logger),該記錄器能夠經過多種途徑保存系統的運行日誌,如經過文件記錄或數據庫記錄,用戶能夠經過修改配置文件靈活地更換日誌記錄方式。在設計各種日誌記錄器時,Sunny公司的開發人員發現須要對日誌記錄器進行一些初始化工做,初始化參數的設置過程較爲複雜,並且某些參數的設置有嚴格的前後次序,不然可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是Sunny公司開發人員面臨的一個難題。java
Sunny公司的開發人員經過對該需求進行分析,發現該日誌記錄器有兩個設計要點:數據庫
(1) 須要封裝日誌記錄器的初始化過程,這些初始化工做較爲複雜,例如須要初始化其餘相關的類,還有可能須要讀取配置文件(例如鏈接數據庫或建立文件),致使代碼較長,若是將它們都寫在構造函數中,會致使構造函數龐大,不利於代碼的修改和維護;編程
(2) 用戶可能須要更換日誌記錄方式,在客戶端代碼中須要提供一種靈活的方式來選擇日誌記錄器,儘可能在不修改源代碼的基礎上更換或者增長日誌記錄方式。設計模式
Sunny公司開發人員最初使用簡單工廠模式對日誌記錄器進行了設計,初始結構如圖1所示:框架
基於簡單工廠模式設計的日誌記錄器結構圖dom
在上圖中,LoggerFactory充當建立日誌記錄器的工廠,提供了工廠方法createLogger()用於建立日誌記錄器,Logger是抽象日誌記錄器接口,其子類爲具體日誌記錄器。其中,工廠類LoggerFactory代碼片斷以下所示:函數
//日誌記錄器工廠
class LoggerFactory {
//靜態工廠方法工具
public static Logger createLogger(String args) { if(args.equalsIgnoreCase("db")) { //鏈接數據庫,代碼省略 //建立數據庫日誌記錄器對象 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } else if(args.equalsIgnoreCase("file")) { //建立日誌文件 //建立文件日誌記錄器對象 Logger logger = new FileLogger(); //初始化文件日誌記錄器,代碼省略 return logger; } else { return null; } }
}
爲了突出設計重點,咱們對上述代碼進行了簡化,省略了具體日誌記錄器類的初始化代碼。在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(具體工廠):它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端調用,返回一個具體產品類的實例。
與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠能夠是接口,也能夠是抽象類或者具體類,其典型代碼以下所示:
interface Factory {
public Product factoryMethod();
}
在抽象工廠中聲明瞭工廠方法但並未實現工廠方法,具體產品對象的建立由其子類負責,客戶端針對抽象工廠編程,可在運行時再指定具體工廠類,具體工廠類實現了工廠方法,不一樣的具體工廠能夠建立不一樣的具體產品,其典型代碼以下所示:
class ConcreteFactory implements Factory {
public Product factoryMethod() {
return new ConcreteProduct();
}
}
在實際使用時,具體工廠類在實現工廠方法時除了建立具體產品對象以外,還能夠負責產品對象的初始化工做以及一些資源和環境配置工做,例如鏈接數據庫、建立文件等。
在客戶端代碼中,只需關心工廠類便可,不一樣的具體工廠能夠建立不一樣的產品,典型的客戶端類代碼片斷以下所示:
……
Factory factory;
factory = new ConcreteFactory(); //可經過配置文件實現
Product product;
product = factory.factoryMethod();
……
能夠經過配置文件來存儲具體工廠類ConcreteFactory的類名,更換新的具體工廠時無須修改源代碼,系統擴展更爲方便。
3 完整解決方案
Sunny公司開發人員決定使用工廠方法模式來設計日誌記錄器,其基本結構如圖3所示:
圖3 日誌記錄器結構圖
在圖3中,Logger接口充當抽象產品,其子類FileLogger和DatabaseLogger充當具體產品,LoggerFactory接口充當抽象工廠,其子類FileLoggerFactory和DatabaseLoggerFactory充當具體工廠。完整代碼以下所示:
//日誌記錄器接口:抽象產品
interface Logger {
public void writeLog();
}
//數據庫日誌記錄器:具體產品
class DatabaseLogger implements Logger {
public void writeLog() { System.out.println("數據庫日誌記錄。"); }
}
//文件日誌記錄器:具體產品
class FileLogger implements Logger {
public void writeLog() { System.out.println("文件日誌記錄。"); }
}
//日誌記錄器工廠接口:抽象工廠
interface LoggerFactory {
public Logger createLogger();
}
//數據庫日誌記錄器工廠類:具體工廠
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() { //鏈接數據庫,代碼省略 //建立數據庫日誌記錄器對象 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; }
}
//文件日誌記錄器工廠類:具體工廠
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//建立文件日誌記錄器對象
Logger logger = new FileLogger(); //建立文件,代碼省略 return logger; }
}
編寫以下客戶端測試代碼:
class Client {
public static void main(String args[]) { LoggerFactory factory; Logger logger; factory = new FileLoggerFactory(); //可引入配置文件實現 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反射(Java Reflection)是指在程序運行時獲取已知名稱的類或已有對象的相關信息的一種機制,包括類的方法、屬性、父類等信息,還包括實例的建立和實例類型的判斷等。在反射中使用最多的類是Class,Class類的實例表示正在運行的Java應用程序中的類和接口,其forName(String className)方法能夠返回與帶有給定字符串名的類或接口相關聯的 Class對象,再經過Class對象的newInstance()方法建立此對象所表示的類的一個新實例,即經過一個類名字符串獲得類的實例。如建立一個字符串類型的對象,其代碼以下:
//經過類名生成實例對象並將其返回
Class c=Class.forName("String");
Object obj=c.newInstance();
return obj;
此外,在JDK中還提供了java.lang.reflect包,封裝了其餘與反射相關的類,此處只用到上述簡單的反射代碼,在此不予擴展。
Sunny公司開發人員建立了以下XML格式的配置文件config.xml用於存儲具體日誌記錄器工廠類類名:
<!— config.xml -->
<?xml version="1.0"?>
<config>
<className>FileLoggerFactory</className>
</config>
爲了讀取該配置文件並經過存儲在其中的類名字符串反射生成對象,Sunny公司開發人員開發了一個名爲XMLUtil的工具類,其詳細代碼以下所示:
//工具類XMLUtil.java
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
public class XMLUtil {
//該方法用於從XML配置文件中提取具體類類名,並返回一個實例對象
public static Object getBean() { try { //建立DOM文檔對象 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dFactory.newDocumentBuilder(); Document doc; doc = builder.parse(new File("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();
return obj;
}
catch(Exception e) {
e.printStackTrace(); return null;
}
}
}
有了XMLUtil類後,能夠對日誌記錄器的客戶端代碼進行修改,再也不直接使用new關鍵字來建立具體的工廠類,而是將具體工廠類的類名存儲在XML文件中,再經過XMLUtil類的靜態工廠方法getBean()方法進行對象的實例化,代碼修改以下:
class Client {
public static void main(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的代碼修改以下:
interface LoggerFactory {
public Logger createLogger(); public Logger createLogger(String args); public Logger createLogger(Object obj);
}
具體工廠類DatabaseLoggerFactory代碼修改以下:
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() { //使用默認方式鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } public Logger createLogger(String args) { //使用參數args做爲鏈接字符串來鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //初始化數據庫日誌記錄器,代碼省略 return logger; } public Logger createLogger(Object obj) { //使用封裝在參數obj中的鏈接字符串來鏈接數據庫,代碼省略 Logger logger = new DatabaseLogger(); //使用封裝在參數obj中的數據來初始化數據庫日誌記錄器,代碼省略 return logger; }
}
//其餘具體工廠類代碼省略
在抽象工廠中定義多個重載的工廠方法,在具體工廠中實現了這些工廠方法,這些方法能夠包含不一樣的業務邏輯,以知足對不一樣產品對象的需求。
6 工廠方法的隱藏
有時候,爲了進一步簡化客戶端的使用,還能夠對客戶端隱藏工廠方法,此時,在工廠類中將直接調用產品類的業務方法,客戶端無須調用工廠方法建立產品,直接經過工廠便可使用所建立的對象中的業務方法。
若是對客戶端隱藏工廠方法,日誌記錄器的結構圖將修改成圖5所示:
圖5 隱藏工廠方法後的日誌記錄器結構圖
在圖5中,抽象工廠類LoggerFactory的代碼修改以下:
//改成抽象類
abstract class LoggerFactory {
//在工廠類中直接調用日誌記錄器類的業務方法writeLog()
public void writeLog() { Logger logger = this.createLogger(); logger.writeLog(); } public abstract Logger createLogger();
}
客戶端代碼修改以下:
class Client {
public static void main(String args[]) { LoggerFactory factory; factory = (LoggerFactory)XMLUtil.getBean(); factory.writeLog(); //直接使用工廠對象來調用產品對象的業務方法 }
}
經過將業務方法的調用移入工廠類,能夠直接使用工廠對象來調用產品對象的業務方法,客戶端無須直接使用工廠方法,在某些狀況下咱們也可使用這種設計方案。
7 工廠方法模式總結
工廠方法模式是簡單工廠模式的延伸,它繼承了簡單工廠模式的優勢,同時還彌補了簡單工廠模式的不足。工廠方法模式是使用頻率最高的設計模式之一,是不少開源框架和API類庫的核心模式。
工廠方法模式的主要優勢以下:
(1) 在工廠方法模式中,工廠方法用來建立客戶所須要的產品,同時還向客戶隱藏了哪一種具體產品類將被實例化這一細節,用戶只須要關心所需產品對應的工廠,無須關心建立細節,甚至無須知道具體產品類的類名。
(2) 基於工廠角色和產品角色的多態性設計是工廠方法模式的關鍵。它可以讓工廠能夠自主肯定建立何種產品對象,而如何建立這個對象的細節則徹底封裝在具體工廠內部。工廠方法模式之因此又被稱爲多態工廠模式,就正是由於全部的具體工廠類都具備同一抽象父類。
(3) 使用工廠方法模式的另外一個優勢是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的接口,無須修改客戶端,也無須修改其餘的具體工廠和具體產品,而只要添加一個具體工廠和具體產品就能夠了,這樣,系統的可擴展性也就變得很是好,徹底符合「開閉原則」。
工廠方法模式的主要缺點以下:
(1) 在添加新產品時,須要編寫新的具體產品類,並且還要提供與之對應的具體工廠類,系統中類的個數將成對增長,在必定程度上增長了系統的複雜度,有更多的類須要編譯和運行,會給系統帶來一些額外的開銷。
(2) 因爲考慮到系統的可擴展性,須要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增長了系統的抽象性和理解難度,且在實現時可能須要用到DOM、反射等技術,增長了系統的實現難度。
在如下狀況下能夠考慮使用工廠方法模式:
(1) 客戶端不知道它所須要的對象的類。在工廠方法模式中,客戶端不須要知道具體產品類的類名,只須要知道所對應的工廠便可,具體的產品對象由具體工廠類建立,可將具體工廠類的類名存儲在配置文件或數據庫中。
(2) 抽象工廠類經過其子類來指定建立哪一個對象。在工廠方法模式中,對於抽象工廠類只須要提供一個建立產品的接口,而由其子類來肯定具體要建立的對象,利用面向對象的多態性和里氏代換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。