1. 程式人生 > >JAVA設計模式(24):建立型-工廠模式【工廠方法模式】(Factory Method)

JAVA設計模式(24):建立型-工廠模式【工廠方法模式】(Factory Method)

簡單工廠模式雖然簡單,但存在一個很嚴重的問題。當系統中需要引入新產品時,由於靜態工廠方法通過所傳入引數的不同來建立不同的產品,這必定要修改工廠類的原始碼,將違背“開閉原則”,如何實現增加新產品而不影響已有程式碼?工廠方法模式應運而生,本文將介紹第二種工廠模式——工廠方法模式。

1 日誌記錄器的設計

       Sunny軟體公司欲開發一個系統執行日誌記錄器(Logger),該記錄器可以通過多種途徑儲存系統的執行日誌,如通過檔案記錄或資料庫記錄,使用者可以通過修改配置檔案靈活地更換日誌記錄方式。在設計各類日誌記錄器時,Sunny公司的開發人員發現需要對日誌記錄器進行一些初始化工作,初始化引數的設定過程較為複雜,而且某些引數的設定有嚴格的先後次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是Sunny

公司開發人員面臨的一個難題。

       Sunny公司的開發人員通過對該需求進行分析,發現該日誌記錄器有兩個設計要點:

       (1) 需要封裝日誌記錄器的初始化過程,這些初始化工作較為複雜,例如需要初始化其他相關的類,還有可能需要讀取配置檔案(例如連線資料庫或建立檔案),導致程式碼較長,如果將它們都寫在建構函式中,會導致建構函式龐大,不利於程式碼的修改和維護;

       (2) 使用者可能需要更換日誌記錄方式,在客戶端程式碼中需要提供一種靈活的方式來選擇日誌記錄器,儘量在不修改原始碼的基礎上更換或者增加日誌記錄方式。

       Sunny公司開發人員最初使用簡單工廠模式對日誌記錄器進行了設計,初始結構如圖1

所示:

 

基於簡單工廠模式設計的日誌記錄器結構圖

       在圖1中,LoggerFactory充當建立日誌記錄器的工廠,提供了工廠方法createLogger()用於建立日誌記錄器,Logger是抽象日誌記錄器介面,其子類為具體日誌記錄器。其中,工廠類LoggerFactory程式碼片段如下所示:

//日誌記錄器工廠  
public 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所示:

工廠方法模式結構圖

       在工廠方法模式結構圖中包含如下幾個角色:

       ● Product(抽象產品):它是定義產品的介面,是工廠方法模式所建立物件的超型別,也就是產品物件的公共父類。

       ● ConcreteProduct(具體產品):它實現了抽象產品介面,某種型別的具體產品由專門的具體工廠建立,具體工廠和具體產品之間一一對應。

       ● Factory(抽象工廠):在抽象工廠類中,聲明瞭工廠方法(Factory Method),用於返回一個產品。抽象工廠是工廠方法模式的核心,所有建立物件的工廠類都必須實現該介面。

       ● ConcreteFactory(具體工廠):它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端呼叫,返回一個具體產品類的例項。

與簡單工廠模式相比,工廠方法模式最重要的區別是引入了抽象工廠角色,抽象工廠可以是介面,也可以是抽象類或者具體類,其典型程式碼如下所示:

public interface Factory {  
    public Product factoryMethod();  
}  

       在抽象工廠中聲明瞭工廠方法但並未實現工廠方法,具體產品物件的建立由其子類負責,客戶端針對抽象工廠程式設計,可在執行時再指定具體工廠類,具體工廠類實現了工廠方法,不同的具體工廠可以建立不同的具體產品,其典型程式碼如下所示:

public 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中,Logger介面充當抽象產品,其子類FileLogger和DatabaseLogger充當具體產品,LoggerFactory介面充當抽象工廠,其子類FileLoggerFactory和DatabaseLoggerFactory充當具體工廠。完整程式碼如下所示:

//日誌記錄器介面:抽象產品  
public 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("檔案日誌記錄。");  
    }  
}  
  
//日誌記錄器工廠介面:抽象工廠  
public 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;  
    }     
} 

       編寫如下客戶端測試程式碼:

public class Client {  
    public static void main(String args[]) {
        LoggerFactory factory = new FileLoggerFactory(); //可引入配置檔案實現  
        Logger 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檔案。關於DOMSAXStAX等技術的詳細學習大家可以參考其他相關資料,在此不予擴充套件。

微笑

擴充套件

關於JavaXML的相關資料,大家可以閱讀Tom MyersAlexander Nakhimovsky所著的《Java XML程式設計指南》一書或訪問developer Works中國中的“Java XML 技術專題”,參考連結:

http://www.ibm.com/developerworks/cn/xml/theme/x-java.html

       Java反射(Java Reflection)是指在程式執行時獲取已知名稱的類或已有物件的相關資訊的一種機制,包括類的方法、屬性、父類等資訊,還包括例項的建立和例項型別的判斷等。在反射中使用最多的類是ClassClass類的例項表示正在執行的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的工具類,其詳細程式碼如下所示:

import java.io.File;  
import org.dom4j.Document;  
import org.dom4j.io.SAXReader;  
public class XMLUtil {  
    //該方法用於從XML配置檔案中提取圖表型別,並返回型別名    
    public static Object getBean() throws Exception {  
        SAXReader reader = new SAXReader();  
        String path = XMLUtil.class.getClassLoader().  
                getResource("com/somnus/designPatterns/factoryMethod/config.xml").getPath();  
        Document document = reader.read(new File(path));  
        String cName = document.selectSingleNode("/config/className").getText();  
        //通過類名生成例項物件並將其返回    
        Class<?> c = Class.forName(cName);    
        Object obj = c.newInstance();    
        return obj;    
    }    
}  

       有了XMLUtil類後,可以對日誌記錄器的客戶端程式碼進行修改,不再直接使用new關鍵字來建立具體的工廠類,而是將具體工廠類的類名儲存在XML檔案中,再通過XMLUtil類的靜態工廠方法getBean()方法進行物件的例項化,程式碼修改如下:

public class Client {  
    public static void main(String[] args) throws Exception {  
        //getBean()的返回型別為Object,需要進行強制型別轉換    
        LoggerFactory factory = (LoggerFactory)XMLUtil.getBean();   
        Logger logger = factory.createLogger();    
        logger.writeLog();    
    }  
}  

       引入XMLUtil類和XML配置檔案後,如果要增加新的日誌記錄方式,只需要執行如下幾個步驟:

       (1) 新的日誌記錄器需要繼承抽象日誌記錄器Logger

       (2) 對應增加一個新的具體日誌記錄器工廠,繼承抽象日誌記錄器工廠LoggerFactory,並實現其中的工廠方法createLogger(),設定好初始化引數和環境變數,返回具體日誌記錄器物件;

       (3) 修改配置檔案config.xml,將新增的具體日誌記錄器工廠類的類名字串替換原有工廠類類名字串;

       (4) 編譯新增的具體日誌記錄器類和具體日誌記錄器工廠類,執行客戶端測試類即可使用新的日誌記錄方式,而原有類庫程式碼無須做任何修改,完全符合“開閉原則”。

      通過上述重構可以使得系統更加靈活,由於很多設計模式都關注系統的可擴充套件性和靈活性,因此都定義了抽象層,在抽象層中宣告業務方法,而將業務方法的實現放在實現層中。

疑問

思考

       有人說:可以在客戶端程式碼中直接通過反射機制來生成產品物件,在定義產品物件時使用抽象型別,同樣可以確保系統的靈活性和可擴充套件性,增加新的具體產品類無須修改原始碼,只需要將其作為抽象產品類的子類再修改配置檔案即可,根本不需要抽象工廠類和具體工廠類。

       試思考這種做法的可行性?如果可行,這種做法是否存在問題?為什麼?


5 過載的工廠方法

       Sunny公司開發人員通過進一步分析,發現可以通過多種方式來初始化日誌記錄器,例如可以為各種日誌記錄器提供預設實現;還可以為資料庫日誌記錄器提供資料庫連線字串,為檔案日誌記錄器提供檔案路徑;也可以將引數封裝在一個Object型別的物件中,通過Object物件將配置引數傳入工廠類。此時,可以提供一組過載的工廠方法,以不同的方式對產品物件進行建立。當然,對於同一個具體工廠而言,無論使用哪個工廠方法,建立的產品型別均要相同。如圖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中,抽象工廠類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. 主要優點

       工廠方法模式的主要優點如下:

       (1) 在工廠方法模式中,工廠方法用來建立客戶所需要的產品,同時還向客戶隱藏了哪種具體產品類將被例項化這一細節,使用者只需要關心所需產品對應的工廠,無須關心建立細節,甚至無須知道具體產品類的類名。

       (2) 基於工廠角色和產品角色的多型性設計是工廠方法模式的關鍵。它能夠讓工廠可以自主確定建立何種產品物件,而如何建立這個物件的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多型工廠模式,就正是因為所有的具體工廠類都具有同一抽象父類。

       (3) 使用工廠方法模式的另一個優點是在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的介面,無須修改客戶端,也無須修改其他的具體工廠和具體產品,而只要新增一個具體工廠和具體產品就可以了,這樣,系統的可擴充套件性也就變得非常好,完全符合“開閉原則”。

      2. 主要缺點

     工廠方法模式的主要缺點如下:

      (1) 在新增新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的複雜度,有更多的類需要編譯和執行,會給系統帶來一些額外的開銷。

      (2) 由於考慮到系統的可擴充套件性,需要引入抽象層,在客戶端程式碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度,且在實現時可能需要用到DOM、反射等技術,增加了系統的實現難度。

       3. 適用場景

       在以下情況下可以考慮使用工廠方法模式:

       (1) 客戶端不知道它所需要的物件的類。在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品物件由具體工廠類建立,可將具體工廠類的類名儲存在配置檔案或資料庫中。

       (2) 抽象工廠類通過其子類來指定建立哪個物件。在工廠方法模式中,對於抽象工廠類只需要提供一個建立產品的介面,而由其子類來確定具體要建立的物件,利用面向物件的多型性和里氏代換原則,在程式執行時,子類物件將覆蓋父類物件,從而使得系統更容易擴充套件。

疑問

練習

使用工廠方法模式設計一個程式來讀取各種不同型別的圖片格式,針對每一種圖片格式都設計一個圖片讀取器,如GIF圖片讀取器用於讀取GIF格式的圖片、JPG圖片讀取器用於讀取JPG格式的圖片。需充分考慮系統的靈活性和可擴充套件性。