1. 程式人生 > >《JAVA與模式》之抽象工廠模式(轉)

《JAVA與模式》之抽象工廠模式(轉)

場景問題

  舉個生活中常見的例子——組裝電腦,我們在組裝電腦的時候,通常需要選擇一系列的配件,比如CPU、硬碟、記憶體、主機板、電源、機箱等。為討論使用簡單點,只考慮選擇CPU和主機板的問題。

  事實上,在選擇CPU的時候,面臨一系列的問題,比如品牌、型號、針腳數目、主頻等問題,只有把這些問題都確定下來,才能確定具體的CPU。

  同樣,在選擇主機板的時候,也有一系列問題,比如品牌、晶片組、整合晶片、匯流排頻率等問題,也只有這些都確定了,才能確定具體的主機板。

  選擇不同的CPU和主機板,是每個客戶在組裝電腦的時候,向裝機公司提出的要求,也就是我們每個人自己擬定的裝機方案。

  在最終確定這個裝機方案之前,還需要整體考慮各個配件之間的相容性。比如:CPU和主機板,如果使用Intel的CPU和AMD的主機板是根本無法組裝的。因為Intel的CPU針腳數與AMD主機板提供的CPU插口不相容,就是說如果使用Intel的CPU根本就插不到AMD的主機板中,所以裝機方案是整體性的,裡面選擇的各個配件之間是有關聯的。

  對於裝機工程師而言,他只知道組裝一臺電腦,需要相應的配件,但是具體使用什麼樣的配件,還得由客戶說了算。也就是說裝機工程師只是負責組裝,而客戶負責選擇裝配所需要的具體的配件。因此,當裝機工程師為不同的客戶組裝電腦時,只需要根據客戶的裝機方案,去獲取相應的配件,然後組裝即可。

使用簡單工廠模式的解決方案

  考慮客戶的功能,需要選擇自己需要的CPU和主機板,然後告訴裝機工程師自己的選擇,接下來就等著裝機工程師組裝電腦了。

  對裝機工程師而言,只是知道CPU和主機板的介面,而不知道具體實現,很明顯可以用上簡單工廠模式或工廠方法模式。為了簡單,這裡選用簡單工廠。客戶告訴裝機工程師自己的選擇,然後裝機工程師會通過相應的工廠去獲取相應的例項物件。

  

原始碼

CPU介面與具體實現

public interface Cpu {
    public void calculate();
}
public class IntelCpu implements Cpu {
    /**
     * CPU的針腳數
     */
    private int pins = 0;
    public  IntelCpu(int pins){
        this.pins = pins;
    }
    @Override
    public void calculate() {
        // TODO Auto-generated method stub
        System.out.println("Intel CPU的針腳數:" + pins);
    }

}

複製程式碼

複製程式碼

public class AmdCpu implements Cpu {
    /**
     * CPU的針腳數
     */
    private int pins = 0;
    public  AmdCpu(int pins){
        this.pins = pins;
    }
    @Override
    public void calculate() {
        // TODO Auto-generated method stub
        System.out.println("AMD CPU的針腳數:" + pins);
    }
}

複製程式碼

主機板介面與具體實現

public interface Mainboard {
    public void installCPU();
}

複製程式碼

public class IntelMainboard implements Mainboard {
    /**
     * CPU插槽的孔數
     */
    private int cpuHoles = 0;
    /**
     * 構造方法,傳入CPU插槽的孔數
     * @param cpuHoles
     */
    public IntelMainboard(int cpuHoles){
        this.cpuHoles = cpuHoles;
    }
    @Override
    public void installCPU() {
        // TODO Auto-generated method stub
        System.out.println("Intel主機板的CPU插槽孔數是:" + cpuHoles);
    }

}

複製程式碼

複製程式碼

public class AmdMainboard implements Mainboard {
    /**
     * CPU插槽的孔數
     */
    private int cpuHoles = 0;
    /**
     * 構造方法,傳入CPU插槽的孔數
     * @param cpuHoles
     */
    public AmdMainboard(int cpuHoles){
        this.cpuHoles = cpuHoles;
    }
    @Override
    public void installCPU() {
        // TODO Auto-generated method stub
        System.out.println("AMD主機板的CPU插槽孔數是:" + cpuHoles);
    }
}

複製程式碼

CPU與主機板工廠類

複製程式碼

public class CpuFactory {
    public static Cpu createCpu(int type){
        Cpu cpu = null;
        if(type == 1){
            cpu = new IntelCpu(755);
        }else if(type == 2){
            cpu = new AmdCpu(938);
        }
        return cpu;
    }
}
public class MainboardFactory {
    public static Mainboard createMainboard(int type){
        Mainboard mainboard = null;
        if(type == 1){
            mainboard = new IntelMainboard(755);
        }else if(type == 2){
            mainboard = new AmdMainboard(938);
        }
        return mainboard;
    }
}

裝機工程師類與客戶類執行結果如下:

public class ComputerEngineer {
    /**
     * 定義組裝機需要的CPU
     */
    private Cpu cpu = null;
    /**
     * 定義組裝機需要的主機板
     */
    private Mainboard mainboard = null;
    public void makeComputer(int cpuType , int mainboard){
        /**
         * 組裝機器的基本步驟
         */
        //1:首先準備好裝機所需要的配件
        prepareHardwares(cpuType, mainboard);
        //2:組裝機器
        //3:測試機器
        //4:交付客戶
    }
    private void prepareHardwares(int cpuType , int mainboard){
        //這裡要去準備CPU和主機板的具體實現,為了示例簡單,這裡只准備這兩個
        //可是,裝機工程師並不知道如何去建立,怎麼辦呢?
        
        //直接找相應的工廠獲取
        this.cpu = CpuFactory.createCpu(cpuType);
        this.mainboard = MainboardFactory.createMainboard(mainboard);
        
        //測試配件是否好用
        this.cpu.calculate();
        this.mainboard.installCPU();
    }
}
public class Client {
    public static void main(String[]args){
        ComputerEngineer cf = new ComputerEngineer();
        cf.makeComputer(1,1);
    }
}

執行結果如下:


  上面的實現,雖然通過簡單工廠方法解決了:對於裝機工程師,只知CPU和主機板的介面,而不知道具體實現的問題。但還有一個問題沒有解決,那就是這些CPU物件和主機板物件其實是有關係的,需要相互匹配的。而上面的實現中,並沒有維護這種關聯關係,CPU和主機板是由客戶任意選擇,這是有問題的。比如在客戶端呼叫makeComputer時,傳入引數為(1,2),執行結果如下:

觀察上面結果就會看出問題。客戶選擇的是Intel的CPU針腳數為755,而選擇的主機板是AMD,主機板上的CPU插孔是938,根本無法組裝,這就是沒有維護配件之間的關係造成的。該怎麼解決這個問題呢?  

引進抽象工廠模式

  每一個模式都是針對一定問題的解決方案。抽象工廠模式與工廠方法模式的最大區別就在於,工廠方法模式針對的是一個產品等級結構;而抽象工廠模式則需要面對多個產品等級結構。

  在學習抽象工廠具體例項之前,應該明白兩個重要的概念:產品族和產品等級。

  所謂產品族,是指位於不同產品等級結構中,功能相關聯的產品組成的家族。比如AMD的主機板、晶片組、CPU組成一個家族,Intel的主機板、晶片組、CPU組成一個家族。而這兩個家族都來自於三個產品等級:主機板、晶片組、CPU。一個等級結構是由相同的結構的產品組成,示意圖如下:

  顯然,每一個產品族中含有產品的數目,與產品等級結構的數目是相等的。產品的等級結構與產品族將產品按照不同方向劃分,形成一個二維的座標系。橫軸表示產品的等級結構,縱軸表示產品族,上圖共有兩個產品族,分佈於三個不同的產品等級結構中。只要指明一個產品所處的產品族以及它所屬的等級結構,就可以唯一的確定這個產品。

  上面所給出的三個不同的等級結構具有平行的結構。因此,如果採用工廠方法模式,就勢必要使用三個獨立的工廠等級結構來對付這三個產品等級結構。由於這三個產品等級結構的相似性,會導致三個平行的工廠等級結構。隨著產品等級結構的數目的增加,工廠方法模式所給出的工廠等級結構的數目也會隨之增加。如下圖:

    那麼,是否可以使用同一個工廠等級結構來對付這些相同或者極為相似的產品等級結構呢?當然可以的,而且這就是抽象工廠模式的好處。同一個工廠等級結構負責三個不同產品等級結構中的產品物件的建立。

  可以看出,一個工廠等級結構可以創建出分屬於不同產品等級結構的一個產品族中的所有物件。顯然,這時候抽象工廠模式比簡單工廠模式、工廠方法模式更有效率。對應於每一個產品族都有一個具體工廠。而每一個具體工廠負責建立屬於同一個產品族,但是分屬於不同等級結構的產品。

抽象工廠模式結構

  抽象工廠模式是物件的建立模式,它是工廠方法模式的進一步推廣。

  假設一個子系統需要一些產品物件,而這些產品又屬於一個以上的產品等級結構。那麼為了將消費這些產品物件的責任和建立這些產品物件的責任分割開來,可以引進抽象工廠模式。這樣的話,消費產品的一方不需要直接參與產品的建立工作,而只需要向一個公用的工廠介面請求所需要的產品。

  通過使用抽象工廠模式,可以處理具有相同(或者相似)等級結構中的多個產品族中的產品物件的建立問題。如下圖所示:

  

  由於這兩個產品族的等級結構相同,因此使用同一個工廠族也可以處理這兩個產品族的建立問題,這就是抽象工廠模式。

  根據產品角色的結構圖,就不難給出工廠角色的結構設計圖。

  可以看出,每一個工廠角色都有兩個工廠方法,分別負責建立分屬不同產品等級結構的產品物件。

  

原始碼

  前面示例實現的CPU介面和CPU實現物件,主機板介面和主機板實現物件,都不需要變化。

  前面示例中建立CPU的簡單工廠和建立主機板的簡單工廠,都不再需要。

  新加入的抽象工廠類和實現類:

public interface AbstractFactory {
    /**
     * 建立CPU物件
     * @return CPU物件
     */
    public Cpu createCpu();
    /**
     * 建立主機板物件
     * @return 主機板物件
     */
    public Mainboard createMainboard();
}
public class IntelFactory implements AbstractFactory {

    @Override
    public Cpu createCpu() {
        // TODO Auto-generated method stub
        return new IntelCpu(755);
    }

    @Override
    public Mainboard createMainboard() {
        // TODO Auto-generated method stub
        return new IntelMainboard(755);
    }

}
public class AmdFactory implements AbstractFactory {

    @Override
    public Cpu createCpu() {
        // TODO Auto-generated method stub
        return new IntelCpu(938);
    }

    @Override
    public Mainboard createMainboard() {
        // TODO Auto-generated method stub
        return new IntelMainboard(938);
    }

}

  裝機工程師類跟前面的實現相比,主要的變化是:從客戶端不再傳入選擇CPU和主機板的引數,而是直接傳入客戶已經選擇好的產品物件。這樣就避免了單獨去選擇CPU和主機板所帶來的相容性問題,客戶要選就是一套,就是一個系列。

public class ComputerEngineer {
    /**
     * 定義組裝機需要的CPU
     */
    private Cpu cpu = null;
    /**
     * 定義組裝機需要的主機板
     */
    private Mainboard mainboard = null;
    public void makeComputer(AbstractFactory af){
        /**
         * 組裝機器的基本步驟
         */
        //1:首先準備好裝機所需要的配件
        prepareHardwares(af);
        //2:組裝機器
        //3:測試機器
        //4:交付客戶
    }
    private void prepareHardwares(AbstractFactory af){
        //這裡要去準備CPU和主機板的具體實現,為了示例簡單,這裡只准備這兩個
        //可是,裝機工程師並不知道如何去建立,怎麼辦呢?
        
        //直接找相應的工廠獲取
        this.cpu = af.createCpu();
        this.mainboard = af.createMainboard();
        
        //測試配件是否好用
        this.cpu.calculate();
        this.mainboard.installCPU();
    }
}

客戶端程式碼:

public class Client {
    public static void main(String[]args){
        //建立裝機工程師物件
        ComputerEngineer cf = new ComputerEngineer();
        //客戶選擇並建立需要使用的產品物件
        AbstractFactory af = new IntelFactory();
        //告訴裝機工程師自己選擇的產品,讓裝機工程師組裝電腦
        cf.makeComputer(af);
    }
}

  抽象工廠的功能是為一系列相關物件或相互依賴的物件建立一個介面。一定要注意,這個介面內的方法不是任意堆砌的,而是一系列相關或相互依賴的方法。比如上面例子中的主機板和CPU,都是為了組裝一臺電腦的相關物件。不同的裝機方案,代表一種具體的電腦系列。

    
  由於抽象工廠定義的一系列物件通常是相關或相互依賴的,這些產品物件就構成了一個產品族,也就是抽象工廠定義了一個產品族。

  這就帶來非常大的靈活性,切換產品族的時候,只要提供不同的抽象工廠實現就可以了,也就是說現在是以一個產品族作為一個整體被切換。

  

在什麼情況下應當使用抽象工廠模式

  1.一個系統不應當依賴於產品類例項如何被建立、組合和表達的細節,這對於所有形態的工廠模式都是重要的。

  2.這個系統的產品有多於一個的產品族,而系統只消費其中某一族的產品。

  3.同屬於同一個產品族的產品是在一起使用的,這一約束必須在系統的設計中體現出來。(比如:Intel主機板必須使用Intel CPU、Intel晶片組)

  4.系統提供一個產品類的庫,所有的產品以同樣的接口出現,從而使客戶端不依賴於實現。

抽象工廠模式的起源

  抽象工廠模式的起源或者最早的應用,是用於建立分屬於不同作業系統的視窗構建。比如:命令按鍵(Button)與文字框(Text)都是視窗構建,在UNIX作業系統的視窗環境和Windows作業系統的視窗環境中,這兩個構建有不同的本地實現,它們的細節有所不同。

  在每一個作業系統中,都有一個視窗構建組成的構建家族。在這裡就是Button和Text組成的產品族。而每一個視窗構件都構成自己的等級結構,由一個抽象角色給出抽象的功能描述,而由具體子類給出不同作業系統下的具體實現。

  
  可以發現在上面的產品類圖中,有兩個產品的等級結構,分別是Button等級結構和Text等級結構。同時有兩個產品族,也就是UNIX產品族和Windows產品族。UNIX產品族由UNIX Button和UNIX Text產品構成;而Windows產品族由Windows Button和Windows Text產品構成。

    系統對產品物件的建立需求由一個工程的等級結構滿足,其中有兩個具體工程角色,即UnixFactory和WindowsFactory。UnixFactory物件負責建立Unix產品族中的產品,而WindowsFactory物件負責建立Windows產品族中的產品。這就是抽象工廠模式的應用,抽象工廠模式的解決方案如下圖:

  

  顯然,一個系統只能夠在某一個作業系統的視窗環境下執行,而不能同時在不同的作業系統上執行。所以,系統實際上只能消費屬於同一個產品族的產品。

  在現代的應用中,抽象工廠模式的使用範圍已經大大擴大了,不再要求系統只能消費某一個產品族了。因此,可以不必理會前面所提到的原始用意。

抽象工廠模式的優點

  • 分離介面和實現

  客戶端使用抽象工廠來建立需要的物件,而客戶端根本就不知道具體的實現是誰,客戶端只是面向產品的介面程式設計而已。也就是說,客戶端從具體的產品實現中解耦。

  • 使切換產品族變得容易

  因為一個具體的工廠實現代表的是一個產品族,比如上面例子的從Intel系列到AMD系列只需要切換一下具體工廠。

抽象工廠模式的缺點

  • 不太容易擴充套件新的產品

  如果需要給整個產品族新增一個新的產品,那麼就需要修改抽象工廠,這樣就會導致修改所有的工廠實現類。