java之設計模式工廠三兄弟之工廠方法模式


【學習難度:★★☆☆☆,使用頻率:★★★★★】

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

 

1 日志記錄器的設計

 

       Sunny軟件公司欲開發一個系統運行日志記錄器(Logger),該記錄器可以通過多種途徑保存系統的運行日志,如通過文件記錄或數據庫記錄,用戶可以通過修改配置文件靈活地更換日志記錄方式。在設計各類日志記錄器時,Sunny公司的開發人員發現需要對日志記錄器進行一些初始化工作,初始化參數的設置過程較為復雜,而且某些參數的設置有嚴格的先后次序,否則可能會發生記錄失敗。如何封裝記錄器的初始化過程並保證多種記錄器切換的靈活性是Sunny公司開發人員面臨的一個難題。

 

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

       (1) 需要封裝日志記錄器的初始化過程,這些初始化工作較為復雜,例如需要初始化其他相關的類,還有可能需要讀取配置文件(例如連接數據庫或創建文件),導致代碼較長,如果將它們都寫在構造函數中,會導致構造函數龐大,不利於代碼的修改和維護;

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

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

 

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

       在圖1中,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的類名,更換新的具體工廠時無須修改源代碼,系統擴展更為方便。

 

疑問

思考

工廠方法模式中的工廠方法能否為靜態方法?為什么?

 

答:不能,因為工廠方法實際上是抽象方法,要求由子類來動態地實現,而動態性與static所聲明的靜態性相沖突

 

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與XML的相關資料,大家可以閱讀Tom Myers和Alexander Nakhimovsky所著的《Java XML編程指南》一書或訪問developer Works 中國中的“Java XML 技術專題”,參考鏈接:

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

 

 

       Java反射(Java Reflection)是指在程序運行時獲取已知名稱的類或已有對象的相關信息的一種機制,包括類的方法、屬性、父類等信息,還包括實例的創建和實例類型的判斷等。在反射中使用最多的類是Class,Class類的實例表示正在運行的Java應用程序中的類和接口,其forName(String className)方法可以返回與帶有給定字符串名的類或接口相關聯的 Class對象,再通過Class對象的newInstance()方法創建此對象所表示的類的一個新實例,即通過一個類名字符串得到類的實例。如創建一個字符串類型的對象,其代碼如下:

  1. //通過類名生成實例對象並將其返回  
  2. Class c=Class.forName("String");  
  3. Object obj=c.newInstance();  
  4. return obj;  

       此外,在JDK中還提供了java.lang.reflect包,封裝了其他與反射相關的類,此處只用到上述簡單的反射代碼,在此不予擴展。

       Sunny公司開發人員創建了如下XML格式的配置文件config.xml用於存儲具體日志記錄器工廠類類名:

  1. <!— config.xml -->  
  2. <?xml version="1.0"?>  
  3. <config>  
  4.     <className>FileLoggerFactory</className>  
  5. </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. 主要優點

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

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

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

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

 

      2. 主要缺點

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

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

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

 

       3. 適用場景

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

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

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

 

疑問

練習

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM