天天看點

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格式的圖檔。需充分考慮系統的靈活性和可擴充性。