【學習難度:★★★★☆,使用頻率:★★★★★】
直接出處:抽象工廠模式
梳理和學習:https://github.com/BruceOuyang/boy-design-pattern
簡書日期: 2018/03/05
簡書首頁:http://www.lxweimin.com/p/0fb891a7c5ed
工廠三兄弟之抽象工廠模式(一)
工廠方法模式通過引入工廠等級結構,解決了簡單工廠模式中工廠類職責太重的問題,但由于工廠方法模式中的每個工廠只生產一類產品,可能會導致系統中存在大量的工廠類,勢必會增加系統的開銷。此時,我們可以考慮將一些相關的產品組成一個“產品族”,由同一個工廠來統一生產,這就是我們本文將要學習的抽象工廠模式的基本思想。
1 界面皮膚庫的初始設計
Sunny軟件公司欲開發一套界面皮膚庫,可以對Java桌面軟件進行界面美化。為了保護版權,該皮膚庫源代碼不打算公開,而只向用戶提供已打包為jar文件的class字節碼文件。用戶在使用時可以通過菜單來選擇皮膚,不同的皮膚將提供視覺效果不同的按鈕、文本框、組合框等界面元素,其結構示意圖如圖1所示:
該皮膚庫需要具備良好的靈活性和可擴展性,用戶可以自由選擇不同的皮膚,開發人員可以在不修改既有代碼的基礎上增加新的皮膚。
Sunny軟件公司的開發人員針對上述要求,決定使用工廠方法模式進行系統的設計,為了保證系統的靈活性和可擴展性,提供一系列具體工廠來創建按鈕、文本框、組合框等界面元素,客戶端針對抽象工廠編程,初始結構如圖2所示:
在圖2中,提供了大量工廠來創建具體的界面組件,可以通過配置文件更換具體界面組件從而改變界面風格。但是,此設計方案存在如下問題:
(1) 當需要增加新的皮膚時,雖然不要修改現有代碼,但是需要增加大量類,針對每一個新增具體組件都需要增加一個具體工廠,類的個數成對增加,這無疑會導致系統越來越龐大,增加系統的維護成本和運行開銷;
(2) 由于同一種風格的具體界面組件通常要一起顯示,因此需要為每個組件都選擇一個具體工廠,用戶在使用時必須逐個進行設置,如果某個具體工廠選擇失誤將會導致界面顯示混亂,雖然我們可以適當增加一些約束語句,但客戶端代碼和配置文件都較為復雜。
如何減少系統中類的個數并保證客戶端每次始終只使用某一種風格的具體界面組件?這是Sunny公司開發人員所面臨的兩個問題,顯然,工廠方法模式無法解決這兩個問題,別著急,本文所介紹的抽象工廠模式可以讓這些問題迎刃而解。
工廠三兄弟之抽象工廠模式(二)
2 產品等級結構與產品族
在工廠方法模式中具體工廠負責生產具體的產品,每一個具體工廠對應一種具體產品,工廠方法具有唯一性,一般情況下,一個具體工廠中只有一個或者一組重載的工廠方法。但是有時候我們希望一個工廠可以提供多個產品對象,而不是單一的產品對象,如一個電器工廠,它可以生產電視機、電冰箱、空調等多種電器,而不是只生產某一種電器。為了更好地理解抽象工廠模式,我們先引入兩個概念:
(1) 產品等級結構:產品等級結構即產品的繼承結構,如一個抽象類是電視機,其子類有海爾電視機、海信電視機、TCL電視機,則抽象電視機與具體品牌的電視機之間構成了一個產品等級結構,抽象電視機是父類,而具體品牌的電視機是其子類。
(2) 產品族:在抽象工廠模式中,產品族是指由同一個工廠生產的,位于不同產品等級結構中的一組產品,如海爾電器工廠生產的海爾電視機、海爾電冰箱,海爾電視機位于電視機產品等級結構中,海爾電冰箱位于電冰箱產品等級結構中,海爾電視機、海爾電冰箱構成了一個產品族。
產品等級結構與產品族示意圖如圖3所示:
在圖3中,不同顏色的多個正方形、圓形和橢圓形分別構成了三個不同的產品等級結構,而相同顏色的正方形、圓形和橢圓形構成了一個產品族,每一個形狀對象都位于某個產品族,并屬于某個產品等級結構。圖3中一共有五個產品族,分屬于三個不同的產品等級結構。我們只要指明一個產品所處的產品族以及它所屬的等級結構,就可以唯一確定這個產品。
當系統所提供的工廠生產的具體產品并不是一個簡單的對象,而是多個位于不同產品等級結構、屬于不同類型的具體產品時就可以使用抽象工廠模式。抽象工廠模式是所有形式的工廠模式中最為抽象和最具一般性的一種形式。抽象工廠模式與工廠方法模式最大的區別在于,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式需要面對多個產品等級結構,一個工廠等級結構可以負責多個不同產品等級結構中的產品對象的創建。當一個工廠等級結構可以創建出分屬于不同產品等級結構的一個產品族中的所有對象時,抽象工廠模式比工廠方法模式更為簡單、更有效率。
抽象工廠模式示意圖如圖4所示:
在圖4中,每一個具體工廠可以生產屬于一個產品族的所有產品,例如生產顏色相同的正方形、圓形和橢圓形,所生產的產品又位于不同的產品等級結構中。如果使用工廠方法模式,圖4所示結構需要提供15個具體工廠,而使用抽象工廠模式只需要提供5個具體工廠,極大減少了系統中類的個數。
工廠三兄弟之抽象工廠模式(三)
3 抽象工廠模式概述
抽象工廠模式為創建一組對象提供了一種解決方案。與工廠方法模式相比,抽象工廠模式中的具體工廠不只是創建一種產品,它負責創建一族產品。抽象工廠模式定義如下:
抽象工廠模式(Abstract Factory Pattern):提供一個創建一系列相關或相互依賴對象的接口,而無須指定它們具體的類。抽象工廠模式又稱為Kit模式,它是一種對象創建型模式。
在抽象工廠模式中,每一個具體工廠都提供了多個工廠方法用于產生多種不同類型的產品,這些產品構成了一個產品族,抽象工廠模式結構如圖5所示:
在抽象工廠模式結構圖中包含如下幾個角色:
AbstractFactory(抽象工廠):它聲明了一組用于創建一族產品的方法,每一個方法對應一種產品。
ConcreteFactory(具體工廠):它實現了在抽象工廠中聲明的創建產品的方法,生成一組具體產品,這些產品構成了一個產品族,每一個產品都位于某個產品等級結構中。
AbstractProduct(抽象產品):它為每種產品聲明接口,在抽象產品中聲明了產品所具有的業務方法。
ConcreteProduct(具體產品):它定義具體工廠生產的具體產品對象,實現抽象產品接口中聲明的業務方法。
在抽象工廠中聲明了多個工廠方法,用于創建不同類型的產品,抽象工廠可以是接口,也可以是抽象類或者具體類,其典型代碼如下所示:
abstract class AbstractFactory {
//工廠方法一
public abstract AbstractProductA createProductA();
//工廠方法二
public abstract AbstractProductB createProductB();
//……
}
具體工廠實現了抽象工廠,每一個具體的工廠方法可以返回一個特定的產品對象,而同一個具體工廠所創建的產品對象構成了一個產品族。對于每一個具體工廠類,其典型代碼如下所示:
class ConcreteFactory1 extends AbstractFactory {
//工廠方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}
//工廠方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
//……
}
與工廠方法模式一樣,抽象工廠模式也可為每一種產品提供一組重載的工廠方法,以不同的方式對產品對象進行創建。
思考
抽象工廠模式是否符合“開閉原則”?【從增加新的產品等級結構和增加新的產品族兩方面進行思考。】
工廠三兄弟之抽象工廠模式(四)
4 完整解決方案
Sunny公司開發人員使用抽象工廠模式來重構界面皮膚庫的設計,其基本結構如圖6所示:
在圖6中,SkinFactory接口充當抽象工廠,其子類SpringSkinFactory和SummerSkinFactory充當具體工廠,接口Button、TextField和ComboBox充當抽象產品,其子類SpringButton、SpringTextField、SpringComboBox和SummerButton、SummerTextField、SummerComboBox充當具體產品。完整代碼如下所示:
//在本實例中我們對代碼進行了大量簡化,實際使用時,界面組件的初始化代碼較為復雜,還需要使用JDK中一些已有類,為了突出核心代碼,在此只提供框架代碼和演示輸出。
//按鈕接口:抽象產品
interface Button {
public void display();
}
//Spring按鈕類:具體產品
class SpringButton implements Button {
public void display() {
System.out.println("顯示淺綠色按鈕。");
}
}
//Summer按鈕類:具體產品
class SummerButton implements Button {
public void display() {
System.out.println("顯示淺藍色按鈕。");
}
}
//文本框接口:抽象產品
interface TextField {
public void display();
}
//Spring文本框類:具體產品
class SpringTextField implements TextField {
public void display() {
System.out.println("顯示綠色邊框文本框。");
}
}
//Summer文本框類:具體產品
class SummerTextField implements TextField {
public void display() {
System.out.println("顯示藍色邊框文本框。");
}
}
//組合框接口:抽象產品
interface ComboBox {
public void display();
}
//Spring組合框類:具體產品
class SpringComboBox implements ComboBox {
public void display() {
System.out.println("顯示綠色邊框組合框。");
}
}
//Summer組合框類:具體產品
class SummerComboBox implements ComboBox {
public void display() {
System.out.println("顯示藍色邊框組合框。");
}
}
//界面皮膚工廠接口:抽象工廠
interface SkinFactory {
public Button createButton();
public TextField createTextField();
public ComboBox createComboBox();
}
//Spring皮膚工廠:具體工廠
class SpringSkinFactory implements SkinFactory {
public Button createButton() {
return new SpringButton();
}
public TextField createTextField() {
return new SpringTextField();
}
public ComboBox createComboBox() {
return new SpringComboBox();
}
}
//Summer皮膚工廠:具體工廠
class SummerSkinFactory implements SkinFactory {
public Button createButton() {
return new SummerButton();
}
public TextField createTextField() {
return new SummerTextField();
}
public ComboBox createComboBox() {
return new SummerComboBox();
}
}
為了讓系統具備良好的靈活性和可擴展性,我們引入了工具類XMLUtil和配置文件,其中,XMLUtil類的代碼如下所示:
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 {
//創建文檔對象
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;
}
}
}
配置文件config.xml中存儲了具體工廠類的類名,代碼如下所示:
<?xml version="1.0"?>
<config>
<className>SpringSkinFactory</className>
</config>
編寫如下客戶端測試代碼:
class Client {
public static void main(String args[]) {
//使用抽象層定義
SkinFactory factory;
Button bt;
TextField tf;
ComboBox cb;
factory = (SkinFactory)XMLUtil.getBean();
bt = factory.createButton();
tf = factory.createTextField();
cb = factory.createComboBox();
bt.display();
tf.display();
cb.display();
}
}
編譯并運行程序,輸出結果如下:
顯示淺綠色按鈕。
顯示綠色邊框文本框。
顯示綠色邊框組合框。
如果需要更換皮膚,只需修改配置文件即可,在實際環境中,我們可以提供可視化界面,例如菜單或者窗口來修改配置文件,用戶無須直接修改配置文件。如果需要增加新的皮膚,只需增加一族新的具體組件并對應提供一個新的具體工廠,修改配置文件即可使用新的皮膚,原有代碼無須修改,符合“開閉原則”。
擴展
在真實項目開發中,我們通常會為配置文件提供一個可視化的編輯界面,類似Struts框架中的struts.xml編輯器,大家可以自行開發一個簡單的圖形化工具來修改配置文件,實現真正的純界面操作。
工廠三兄弟之抽象工廠模式(五)
5 “開閉原則”的傾斜性
Sunny公司使用抽象工廠模式設計了界面皮膚庫,該皮膚庫可以較為方便地增加新的皮膚,但是現在遇到一個非常嚴重的問題:由于設計時考慮不全面,忘記為單選按鈕(RadioButton)提供不同皮膚的風格化顯示,導致無論選擇哪種皮膚,單選按鈕都顯得那么“格格不入”。Sunny公司的設計人員決定向系統中增加單選按鈕,但是發現原有系統居然不能夠在符合“開閉原則”的前提下增加新的組件,原因是抽象工廠SkinFactory中根本沒有提供創建單選按鈕的方法,如果需要增加單選按鈕,首先需要修改抽象工廠接口SkinFactory,在其中新增聲明創建單選按鈕的方法,然后逐個修改具體工廠類,增加相應方法以實現在不同的皮膚中創建單選按鈕,此外還需要修改客戶端,否則單選按鈕無法應用于現有系統。
怎么辦?答案是抽象工廠模式無法解決該問題,這也是抽象工廠模式最大的缺點。在抽象工廠模式中,增加新的產品族很方便,但是增加新的產品等級結構很麻煩,抽象工廠模式的這種性質稱為“開閉原則”的傾斜性。“開閉原則”要求系統對擴展開放,對修改封閉,通過擴展達到增強其功能的目的,對于涉及到多個產品族與多個產品等級結構的系統,其功能增強包括兩方面:
(1) 增加產品族:對于增加新的產品族,抽象工廠模式很好地支持了“開閉原則”,只需要增加具體產品并對應增加一個新的具體工廠,對已有代碼無須做任何修改。
(2) 增加新的產品等級結構:對于增加新的產品等級結構,需要修改所有的工廠角色,包括抽象工廠類,在所有的工廠類中都需要增加生產新產品的方法,違背了“開閉原則”。
正因為抽象工廠模式存在“開閉原則”的傾斜性,它以一種傾斜的方式來滿足“開閉原則”,為增加新產品族提供方便,但不能為增加新產品結構提供這樣的方便,因此要求設計人員在設計之初就能夠全面考慮,不會在設計完成之后向系統中增加新的產品等級結構,也不會刪除已有的產品等級結構,否則將會導致系統出現較大的修改,為后續維護工作帶來諸多麻煩。
6 抽象工廠模式總結
抽象工廠模式是工廠方法模式的進一步延伸,由于它提供了功能更為強大的工廠類并且具備較好的可擴展性,在軟件開發中得以廣泛應用,尤其是在一些框架和API類庫的設計中,例如在Java語言的AWT(抽象窗口工具包)中就使用了抽象工廠模式,它使用抽象工廠模式來實現在不同的操作系統中應用程序呈現與所在操作系統一致的外觀界面。抽象工廠模式也是在軟件開發中最常用的設計模式之一。
- 主要優點
抽象工廠模式的主要優點如下:
(1) 抽象工廠模式隔離了具體類的生成,使得客戶并不需要知道什么被創建。由于這種隔離,更換一個具體工廠就變得相對容易,所有的具體工廠都實現了抽象工廠中定義的那些公共接口,因此只需改變具體工廠的實例,就可以在某種程度上改變整個軟件系統的行為。
(2) 當一個產品族中的多個對象被設計成一起工作時,它能夠保證客戶端始終只使用同一個產品族中的對象。
(3) 增加新的產品族很方便,無須修改已有系統,符合“開閉原則”。
- 主要缺點
抽象工廠模式的主要缺點如下:
增加新的產品等級結構麻煩,需要對原有系統進行較大的修改,甚至需要修改抽象層代碼,這顯然會帶來較大的不便,違背了“開閉原則”。
- 適用場景
在以下情況下可以考慮使用抽象工廠模式:
(1) 一個系統不應當依賴于產品類實例如何被創建、組合和表達的細節,這對于所有類型的工廠模式都是很重要的,用戶無須關心對象的創建過程,將對象的創建和使用解耦。
(2) 系統中有多于一個的產品族,而每次只使用其中某一產品族。可以通過配置文件等方式來使得用戶可以動態改變產品族,也可以很方便地增加新的產品族。
(3) 屬于同一個產品族的產品將在一起使用,這一約束必須在系統的設計中體現出來。同一個產品族中的產品可以是沒有任何關系的對象,但是它們都具有一些共同的約束,如同一操作系統下的按鈕和文本框,按鈕與文本框之間沒有直接關系,但它們都是屬于某一操作系統的,此時具有一個共同的約束條件:操作系統的類型。
(4) 產品等級結構穩定,設計完成之后,不會向系統中增加新的產品等級結構或者刪除已有的產品等級結構。
練習
Sunny軟件公司欲推出一款新的手機游戲軟件,該軟件能夠支持Symbian、Android和Windows Mobile等多個智能手機操作系統平臺,針對不同的手機操作系統,該游戲軟件提供了不同的游戲操作控制(OperationController)類和游戲界面控制(InterfaceController)類,并提供相應的工廠類來封裝這些類的初始化過程。軟件要求具有較好的擴展性以支持新的操作系統平臺,為了滿足上述需求,試采用抽象工廠模式對其進行設計。
練習會在我的github上做掉