【設計模式】簡單工廠-工廠方法-抽象工廠


本文主要介紹工廠模式,首先是最基本的簡單工廠(嚴格地說這不是標准的設計模式),然后是工廠方法模式和抽象工廠模式。

1. 簡單工廠

2. 工廠方法模式

3. 抽象工廠模式

在這里共同使用的場景是一個數據轉換的應用:某客戶A要把自己電腦某程序中的數據導出,再導入給B,而導出數據的格式是不確定的,可以是Excel,可以是XML等等。

 

簡單工廠

1. 面向接口的編程

在Java應用開發中,要“面向接口編程”,而接口的思想是“封裝隔離”,這里的封裝不是對數據的封裝,而是指對被隔離體的行為或職責的封裝,並把外部調用和內部實現隔離開,只要接口不變,內部實現的變化就不會影響到外部應用,從而使得系統更靈活,具有更好的擴展性和可維護性,“接口是系統可插拔性的保證”。

2. 不使用工廠模式時的接口使用方法

對上述場景做一下簡化,這個轉移數據的程序完成之后直接交由客戶使用,不再進行二次開發,說得通俗一點,程序的接口已經指定,必須使用Excel或XML這兩種格式之一進行導出,在使用本程序的時候,客戶直接選擇是哪一種格式,在這里,僅關心客戶端A的行為,也就是怎么導出數據。

假設有一個接口叫IExportFile,實現類ExportExcel,實現了方法export(),那么創建實現該接口的實例的時候,會這樣寫,

首先是IExportFile接口的內容:

public interface IExportFile {
        public void export();
}

實現類ExportExcel的內容:

public class ExportExcel implements IExportFile {
        @Override
        public void export() {
           // TODO Auto-generated method stub
           System.out.println("輸出excel格式數據...");
        }
}

在main函數中:

public static void main(String [] args){
       ExportExcel expFile = new ExportExcel();
       expFile.export();
}

這樣的調用方式有一個不方便的地方,客戶端在調用的時候,不僅僅使用了接口,還確切的知道了具體的實現類是哪個,試想,對於一個使用者而言,我只指定說我要excel格式的數據就可以了,還需要知道導出類的名字是ExportExcel嗎?這就失去了使用接口的一部分意義(只實現了多態,而沒有實現封裝隔離),而且直接使用

ExportExcel expFile = new ExportExcel();

這樣的語句就可以了。

如下圖所示,客戶端需要知道所有的模塊:

 

而較為好的編程方式,是客戶端只知道接口而不知道實現類是哪個,在這個例子中,客戶端只知道是使用了IExportFile接口,而不關心具體誰去實現,也不知道是怎么實現的,怎么做到這一點呢,可以使用簡單工廠來解決。

3. 簡單工廠

定義說明:簡單工廠提供一個創建對象實例的功能,而無須關心具體實現,被創建實例的類型可以是接口、抽象類或是具體的類,外部不應該知道實現類,而內部是必須要知道的,所以可以在內部創建一個類,在這個類的內部生成接口並返回給客戶端。

具體實現方法:現在該接口有兩個實現類:ExportExcel和ExportXML(內容與ExportExcel對應,不再給出具體實現),下面來看簡單工廠的實現:

public class ExportFactory {
        public static IExportFile createExportFormat(int index){
           IExportFile expFile = null;
           if(index == 0){
               expFile = new ExportExcel();
           }
           else if(index == 1){
               expFile = new ExportXML();
           }
           return expFile;
        }
}

在客戶端呢,通過傳入參數來生成具體實現,這樣只需要告訴客戶數字與輸出數據格式的對應關系,而不用讓客戶去知道實現類是哪個:

public static void main(String [] args){
        IExportFile expFile = ExportFactory.createExportFormat(0);
        expFile.export();
}

在客戶端,已經被屏蔽了具體的實現:

 

4. 可配置的簡單工廠

上面的例子中,有兩個實現類,傳入的參數index可以取值0或1,如果再增加一種實現類,就需要修改工廠類的方法,肯定不是一種好的實現方法,這里提供一種解決方案,通過java的反射來完成(我用反射寫程序曾經被IBM的工程師批評過,因為尤其在繼承關系比較復雜的情況下會出現一些安全問題,這也就是使用反射經常要捕獲SecurityException的原因,所以要慎重選用),比如有一個xml或properties配置文件,為方便演示,這里使用properties文件,命名為factory.properties,里面有一行配置的屬性:

ExportClass=ExportExcel(我的范例是在默認包里做的,正式工程中需要加適當的前綴,如org.zhfch.export.ExportExcel)

此時工廠類內容如下:

public class ExportFactory {
        public static IExportFile createExportFormat(){
            IExportFile expFile = null;
            Properties p = new Properties();
            try {
                p.load(Factory.class.getResourceAsStream("factory.properties"));
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            try {
                expFile = (IExportFile) Class.forName(p.getProperty("ExportClass")).newInstance();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return expFile;
        }
}

此時,可以直接在配置文件中進行配置,無需再去在程序里傳參了。

5. 模式說明

該類用於創造接口,因此命名為工廠,通常不需要創建工廠類實例,使用靜態方法,這樣的工廠也被稱為靜態工廠,而且簡單工廠理論上可以創造任何類,也叫萬能工廠,類名通常為“模塊名+Factory”,如MultimediaFactory,方法名通常為“get+接口名稱”或“create+接口名稱”。

工廠類僅僅用來選擇實現類,而不創造具體實現方法,而具體選用哪個實現類,可以選用函數傳參、讀取配置文件、讀取程序運行的某中間結果來選擇。

簡單工廠的優點是比較好地實現了組件封裝,同時降低了客戶端與實現類的耦合度,缺點是客戶端在配置的時候需要知道許多參數的意義,增加了復雜度,另外如果想對工廠類進行繼承來覆寫創建方法,就不能夠實現了。

擴展概念-抽象工廠模式:抽象工廠里面有多個用於選擇並創建對象的方法,並且創建的這些對象之間有一定聯系,共同構成一個產品簇所需的部件,如果抽象工廠退化到只有一個實現,就是簡單工廠了。

擴展概念-工廠方法模式:工廠方法模式把選擇實現的功能放到子類里去實現,如果放在父類里就是簡單工廠。

 

工廠方法模式(Factory Method

1. 框架的相關概念

框架就是能完成一定功能的半成品軟件,它不能完全實現用戶需要的功能,需要進一步加工,才能成為一個滿足用戶需要的、完整的軟件。框架級的軟件主要面向開發人員而不是最終用戶。

使用框架能加快應用開發進度,並且能提供一個精良的程序架構。

而設計模式是一個比框架要抽象得多的概念(框架已經是一個產品,而設計模式還是個思想),框架目的明確,針對特定領域,而設計模式更加注重解決問題的思想方法。

2. 工廠方法模式

現在把簡單工廠的那個場景稍作變化,現在只是做一個易於擴展的程序框架,在編寫導出文件的行為時,我們並不知道具體要導出成什么文件,首先有一些約定俗成的格式,比如Excel、XML,但也可能是txt,甚至是現在還完全想不到的格式,換句話說,即使在這個半成品軟件的內部,也不一定知道該去選擇什么實現類,這個時候,就可以使用工廠方法模式。

工廠方法模式:定義一個用於創建對象的接口,讓子類決定實例化哪一個類,Factory Method將一個類的實例化延遲到子類中進行。

在這個場景中,我們需要的導出文件的接口,仍然是IExportFile:

public interface IExportFile {
    public void export();
}

先來看ExportExcel類的實現:

public class ExportExcel implements IExportFile {
    @Override
    public void export() {
       // TODO Auto-generated method stub
       System.out.println("輸出excel格式數據...");
    }
}

重點是一個生成器類,在該類中聲明工廠方法,這個工廠方法通常是protected類型,返回一個IExportFile類型的實例化對象並在這個類的其他方法中被使用,而這個工廠方法多是抽象的,在子類中去返回實例化的IExportFile對象:

public abstract class ExportCreator {
    protected abstract IExportFile factoryMethod();
    public void export(){
       IExportFile expFile = factoryMethod();
       expFile.export();
    }
}

這樣,這個抽象類的export()方法在不知道具體實現的情況下實現了數據的導出操作。如果這時候要使用Excel這種導出格式,在已經有IExportFile對應的實現類ExportExcel之后(沒有的話就先創建),再創建一個ExportCreator的子類,覆寫factoryMethod方法來返回ExportExcel的對象實例就可以了:

在ExportCreator的子類中選擇IExportFile的實現:

public class ExportExcelCreator extends ExportCreator{
    @Override
    protected IExportFile factoryMethod() {
       // TODO Auto-generated method stub
       return new ExportExcel();
    }
}

使用時在main函數中調用:

public static void main(String [] args){
    ExportCreator ec = new ExportExcelCreator();
    ec.export();
}

模式的類結構圖如下:

 

 

3. 工廠方法模式與IoC/DI

所謂的控制反轉/依賴注入,要理解:是某個對象依賴於IoC/DI容器來提供外部資源,或者說IoC/DI容器向某個對象注入外部資源,IoC/DI容器控制對象實例的創建。比如有一個操作文件格式的邏輯類:FileFormatLogic,該類中有一個FileFormatDAO類型的變量fileFormatDao,那么此時,fileFormatDao就是FileFormatLogic所需的外部資源,正常思路是在FileFormatLogic中使用fileFormatDao = new FileFormatDAO()來創建對象,這是正向的,而反轉是說,FileFormatLogic不再主動地去創建對象,而是被動的等IoC/DI容器給它一個FileFormatDAO的對象實例。

工廠方法模式與IoC/DI的思想有類似的地方:

現在有一個類A,需要一個接口C的實例,並使用依賴注入的方法獲得,A代碼如下:

public class A {
    //等待被注入的對象c
    private C c = null;
    //注入對象c的方法
    public void setC(C c){
       this.c = c;
    }
    //使用從外部注入的c做一些事情
    public void ta(){
       c.tc();
    }
}

接口C很簡單:

public interface C {
    public void tc();
}

那么怎么能把IoC/DI和工廠方法模式的思想聯系到一起呢?需要對A做一點改動,現在修改A的內容:

public abstract class A {
    //需要C實例時調用,相當於從子類注入
    protected abstract C createC();
    //需要使用C實例時,調用方法讓子類提供一個
    public void ta(){
       createC().tc();
    }
}

createC()就是一個工廠方法,等待在子類中進行注入(這和我們常用的依賴注入並不相同,思想相似)。

4. 參數化的工廠方法

前面的例子中,我們使用的工廠方法都是抽象的,但它必須抽象嗎?其實不是的,我們可以在工廠方法中提供一些默認的實例選擇(通過判斷傳入的index參數生成不同的實例),需要擴展時再在子類中進行覆寫,參數化的創建器如下:

public class ExportCreator {
    protected IExportFile factoryMethod(int index){
       IExportFile expFile = null;
       if(index == 0){
           expFile = new ExportExcel();
       }
       else if(index == 2){
           expFile = new ExportXML();
       }
       return expFile;
    }
    public void export(int index){
       IExportFile expFile = factoryMethod(index);
       expFile.export();
    }
}

如果這個時候突然又需要導出Txt格式的數據,則需要繼承這個創建器類,覆寫工廠方法,特殊的地方是,如果傳入的參數指示並不是txt的實現,則調用父類的默認方法來選擇對象:

public class ExportTxtCreator extends ExportCreator{
    @Override
    protected IExportFile factoryMethod(int index) {
       // TODO Auto-generated method stub
       IExportFile expFile = null;
       if(index == 2){
           expFile = new ExportTxt();
       }
       else{
           expFile = super.factoryMethod(index);
       }
       return expFile;
    }
}

5. 模式說明

工廠方法的本質就是把選擇實現方式延遲到子類來完成,它的優點是可以在不知道具體實現的情況下編程、易於擴展,缺點是具體產品對象和工廠方法是耦合的。

看最后“參數化的工廠方法”中的ExportCreator創建器的實現,如果把export方法去掉,再為工廠方法加上static修飾,就變成了簡單工廠,他們本質上是類似的。

何時選用:如果一個類需要創建某個接口的對象,但又不知道具體的實現,或者本來就希望子類來創建所需對象的時候選用。

 

抽象工廠模式(Abstract Factory

1. 場景描述

對於最初的場景,在簡單工廠和工廠方法中,都只是使用了客戶A的導出,現在要考慮在客戶B那里導入了,這兩個模塊分別由A和B各自實現,我們可以仿照簡單工廠中的代碼來完成導入功能,但很容易想到問題:A用Excel格式導出,B卻調用XML格式導入怎么辦?

2. 抽象工廠模式

上面描述的問題出現根源是:導入和導出這兩個模塊是相互依賴的,而抽象工廠就是要提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的實現類。

換言之,抽象工廠主要起一個約束作用,提供所有子類的一個統一的外觀,來讓客戶使用。

導出導入的接口如下:

public interface IExportFile {
    public void export();
}
public interface IImportFile {
    public void iimport();//import是關鍵字,所以加一個i
}

抽象工廠(是一個接口)定義如下:

public interface AbstractFactory {
    public IExportFile createExport();
    public IImportFile createImport();
}

為每一種數據轉移方案添加一種具體實現,數據轉移方案1:

public class Schema1 implements AbstractFactory {
    @Override
    public IExportFile createExport() {
       // TODO Auto-generated method stub
       return new ExportExcel();
    }
    @Override
    public IImportFile createImport() {
       // TODO Auto-generated method stub
       return new ImportExcel();
    }
}

數據轉移方案2:

public class Schema2 implements AbstractFactory {
    @Override
    public IExportFile createExport() {
       // TODO Auto-generated method stub
       return new ExportXML();
    }
    @Override
    public IImportFile createImport() {
       // TODO Auto-generated method stub
       return new ImportXML();
    }
}

對於一個全局的數據轉移的類,接收一個指定的數據轉移方案作為參數來進行數據轉移:

public class DataTransfer {
    public void transfer(AbstractFactory schema){
       //當然應該把導出的內容傳給導入的模塊,此處從略了
       schema.createExport().export();
       schema.createImport().iimport();
    }
}

使用時創建一個具體的解決方案並傳給這個類去進行處理:

public static void main(String [] args){
    DataTransfer dt = new DataTransfer();
    AbstractFactory schema = new Schema1();
    dt.transfer(schema);
}

模式結構圖如下:

3. 抽象工廠模式與DAO

DAO是J2EE中的一個標准模式,解決訪問數據對象所面臨的諸如數據源不同、存儲類型不同、數據庫版本不同等一系列問題。對邏輯層來說,他可以直接訪問DAO而不用關心這么多的不同,換言之,借助DAO,邏輯層可以以一個統一的方式來訪問數據。事實上,在實現DAO時,最常見的實現策略就是工廠,尤以抽象工廠居多。

比如訂單處理的模塊,訂單往往分為訂單主表和訂單明細表,現在業務對象需要操作訂單的主記錄和明細記錄,而數據底層的數據存儲方式可能是不同的,比如可能是使用關系型數據庫來存儲,也可能是使用XML來進行存儲。說到這里就很容易和前面的例子結合在一起了吧?原理可以說是完全一樣的,這里給出抽象工廠實現策略的結構示意圖,就不再給出代碼實現了:

4. 可擴展的抽象工廠

現在的抽象工廠,如果要進行擴展,比如在數據導出和導入之間要加一個數據清洗的模塊,就比較麻煩了,從接口到每一個實現都需要添加一個新的方法,有一種較為靈活的實現方式,但是卻有一定安全問題。

首先,抽象工廠不是要為每一個模塊返回一個實例嗎,每增加一個模塊就要增加一個方法不易於擴展,那么就干脆只用一個方法,根據參數來返回不同的類型,再強制轉化成我們需要的模塊對象,顯而易見,這時候抽象工廠這個唯一的方法就需要返回一個Object類型的對象了:

public interface AbstractFactory {
    public Object createModule(int module);
}

在具體的實現方案中,就要根據參數返回不同的模塊實例,比如在方案1(Excel格式的數據轉移方案)中,可以指定,參數為0時返回Excel導出模塊的實例,參數為1時返回Excel導入模塊的實例:

public class Schema1 implements AbstractFactory {
    @Override
    public Object createModule(int module) {
       // TODO Auto-generated method stub
       Object obj = null;
       if(module == 0){
           obj = new ExportExcel();
       }
       else if(module == 1){
           obj = new ImportExcel();
       }
       /**
        * 如果此事要添加一個清晰數據的模塊,則可以在這里添加
        * else if(module == 2){
        *     obj = new CleanExcel();
        * }
        */
       return obj;
    }
}

數據處理的全局類修改如下:

public class DataTransfer {
    public void transfer(AbstractFactory schema){
       //當然應該把導出的內容傳給導入的模塊,此處從略了
       ((IExportFile)schema.createModule(0)).export();
       /**
        * 添加清洗模塊時在這里添加:
        * ((ICleanFile)schema.createModule(2)).clean();
        */
       ((IImportFile)schema.createModule(1)).iimport();
    }
}

main函數的調用方式不變,所謂的不安全就是指,如果指定返回參數0所對應的對象(IExportFile),但是卻強制轉化成IImportFile,就會拋異常,但這種方法確實比之前的方法靈活了許多,是否應該選用就要看具體應用設計上的權衡了。

5. 模式說明

AbstractFactory通常是一個接口,而不是抽象類(也可以實現為抽象類,但是不建議)!而在AbstractFactory中創建對象的方式,可以看做是工廠方法,這些工廠方法的具體實現延遲到子類具體的工廠中去,換句話說,經常會使用工廠方法來實現抽象工廠。

切換產品簇:抽象工廠的一系列對象通常是相互依賴或相關的,這些對象就構成一個產品簇,切換產品簇只需要提供不同的抽象工廠的實現就可以了。把產品簇作為一個整體來進行切換。甚至可以說,抽象工廠模式的本質,就是選擇產品簇的實現。

抽象工廠模式的優點:分離了接口和實現,使得切換產品簇非常方便。

抽象工廠模式的缺點:不易擴展(使用前面提到的擴展方法又不夠安全),使得類層次結構變得復雜。

通常一個產品系列只需要一個實例就夠了,所以具體的工廠實現可以用單例模式來實現。

 

注:參考書目為清華大學出版社出版的《研磨設計模式》一書。


免責聲明!

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



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