對象創建:Abstract Factory
目的
提供一個可以創建一族關聯的或者依賴的對象,而不需要指定它們具體的類。
有時也可稱爲
Kit
驅動
假設有一個用戶接口工具箱,它可以支持多種不同的觀感標準,例如Motif和Presentation Manager。不同的觀感標準定義了不同的外觀和用戶界面行爲,例如滾動條、窗口和按鈕。要想在不同的觀感標準之間方便的遷移,一個應用不應該寫死自己的widget。在整個應用裏到處實例化某一種觀感標準的widget,導致後期很難修改爲其它的觀感標準。
我們可以使用一個抽象的WidgetFactory
來解決這個問題,WidgetFactory
爲每種基本的widget聲明瞭創建接口。同樣,每一種widget也有一個相應的抽象類,具體的子類會針對不同的觀感標準的widget具體的實現。WidgetFactory提供的接口可以真對每一個抽象的widget類創建widget對象。客戶端可以調用這些接口來獲得widget實例,但是客戶端並不知道它們具體用了哪一個類。因此,客戶端可以獨立於觀感標準。
對於每個觀感標準,都有一個WidgetFactory與之對應。每一個子類實現了具體的操作來創建該觀感標準的widget。例如,在MotifWidgetFactory中的CreateScrollBar
操作可以實例化並且返回一個Motif的滾動條。客戶端僅僅通過WidgetFactory接口來創建widget,而無需瞭解到底是哪種觀感標準類實現了這個widget。也就是,客戶端直接交互的是抽象類的接口,而不是某一個具體的類。
WidgetFactory同樣加強了某一標準下,不同widget的依賴關係。例如,一個Motif滾動條應該和Motif按鈕和Motif文本編輯器同時使用,這種約束關係自然而然的被加強了,因爲它們都通過MotifWidgetFactory來實現。
適用
可以在一下情況下適用Abstract Factory模式:
- 系統需要獨立於它的“產品(product)”如何被創建、組成和表示時。
- 系統面對不同的產品家族,只需要配置其中一個時。
- 一個家族的關聯產品對象需要被一起配合使用時,你需要強化這種限制時。
- 你希望提供一個產品類庫,你只想暴露它們的接口,而不是它們的實現時。
結構
成員
- AbstractFactory(WidgetFactory):對創建抽象產品對象的接口的聲明。
- ConcreteFactory(MotifWidgetFactory,PMWidgetFactory):實現創建具體產品對象的操作。
- AbstractProduct(Window,ScrollBar):給某種產品對象聲明一個接口。
- ConcreteProduct(MotifWindow,MotifScrollBar):定義一個產品對象,這個對象可以被其相應的ConcreteFactory創建;實現AbstractProduct接口。
- Client:只使用AbstractFactory和AbstractProduct提供的接口。
合作
- 正常情況下只有一個ConcreteFactory實例在運行時被創建。這個具體的工廠可以創建特定的產品。客戶端要想創建不同的產品,需要使用不同的具體工廠。
- AbstractFactory將創建產品對象的過程交給ConcreteFactory子類。
結果
Abstract Factory模式有以下的優點和責任:
- 它隔離了具體的類。Abstract Factory模式幫助你控制應用創建的對象。因爲一個工廠封裝了創建一個產品對象的責任和過程,它將客戶端和類的具體實現隔離開來。客戶端通過抽象接口來操作實例。產品的類的具體名字被隔離在具體的工廠裏,它們不會出現在客戶端中。
- 它使得產品家族的切換變得容易。在應用中,具體工廠的類只出現一次,也就是在它被實例化的時候。這就使得在應用中改變具體工廠類變得很容易。應用可以修改具體的工廠,來使用不同的產品配置。因爲一個抽象工廠會包含某一個家族的所有產品,所以整個產品家族可以通過這個抽象工廠一次性修改。在我們的例子中,我們能夠從Motif widget轉換成Presentation Manager widget,只需要將抽象工廠修改成對應的具體的工廠。
- 提高了產品的一致性。當一個家族的產品需要一起使用時,對應用而言,應該每次只使用某一個家族的產品。AbstractFactory可以用來加強這種關係。
- 不易支持新的產品。擴展已有的抽象工廠來使其能夠創建新品種的產品是不太容易的。因爲在AbstractFactory裏,提供的接口已經固定了能夠創建的產品類型。支持新類型的產品需要擴展工廠的接口,這包括AbstractFactory類和其子類的接口。我們會在下面具體實現時,提供一個解決這個問題的方法。
實現
這裏介紹一下實現Abstract Factory模式的一些技術。
單例模式
一個應用對某個產品家族而言,只需要一個ConcreteFactory實例即可。所以,最好情況下實現爲一個Singleton。
創建product
AbstractFactory只聲明創建產品的接口,具體的創建過程交給對應的ConcreateProduct子類來實現。最常用的方法是對每個產品使用工廠方法Factory Method
。一個具體的工廠將會重寫每個工廠方法來生產具體的產品。這種實現方法很簡單,每個產品家族都需要有自己的具體的工廠子類,儘管這些產品家族之間不盡相同。
如果有許多產品家族,concrete factory可以利用Prototype模式來實現。Concrete factory可以利用每個產品的原型實例來初始化。基於Prototype的方法消除了對每個新的產品家族都需要實現新的concrete factory的麻煩。
這裏有一種使用Smalltalk來實現基於Prototype的factory的方法。Concrete factory 將prototype存儲在一個叫partCatalog
的字典裏。方法make
會獲取這個原型並且進行克隆:
make: partName
(partCatalog at: partName) copy
Concrete factory有一個向catalog中添加part的方法:
addPart: partTemplate named: partName
partCatalog at: partName put: partTemplate
可以用一個標識符將Prototype加入factory:
aFactory addPart: aPrototype named: #ACMEWidget
這種基於Prototype的改進可以在一些特殊的編程語言中實現,這些語言將類看做基本的對象,例如Smalltalk、Objective C。在這個例子中,你可以將類看做一種退化的factory,這個factory它只能構建一種product。你可以將product類像變量一樣存儲在concrete factory中,因此factory可以創建各種各樣的product,就像prototype一樣。
定義可擴展元素
AbstractFactory一般會對每個種類的product定義不同的函數,product的種類一般會在函數名稱上顯示出來。添加新的product需要修改Abstract提供的接口和所有相關的類。
一個更爲靈活但是不太安全的做法是給函數添加參數,這些參數決定了這些函數將要構建哪個product。這個參數可以是類標識符、整數、數組或者其它可以指明這個product的標記。這樣,AbstractFactory 只需要一個函數“Make”,它的參數指明瞭需要構建的對象。這種方法已經在先前討論的基於Prototype的factory中討論過。
這個方法在動態類型的語言中更易使用,例如Smalltalk。在靜態語言中如C++,則需要所有的product都有公共的抽象基本類,或者這些product可以被client安全地強制轉化成正確的類型。在Factory Method章節中,會講述如何用C++實現參數化的操作。
即使不需要強制類型轉化,因爲繼承導致的問題依然存在:所有返回給client的product都有着相同的抽象類型和相同的抽象接口。Client將無法區別和安全的推斷這些product的類。如果client需要調用子類的函數,則在這個抽象類中無法使用。儘管可以使用向下類型轉換,例如在C++中的dynamic_cast,但是這不總是可行的,因爲向下投影可能會失敗。這是在設計高靈活性和擴展性接口的是需要考慮的權衡問題。
樣例代碼
我們將使用Abstract Factory模式來創建一個maze,maze遊戲已經在上一章節中討論過。
類MazeFactory可以創建maze的組件。它可以創建room、wall和door。其它程序可以讀取文件中的設計計劃,使用MazeFactory來創建相應的maze。其它程序也可以利用MazeFactory來隨機的創建一個maze。創建maze的程序會將MazeFactory作爲一個變量,從而程序員可以指定room、wall和door的種類。
calss MazeFactory{
public:
MazeFactory();
virtual Maze* MakeMaze() const
{return new Maze;}
virtual Wall* MakeWall() const
{return new Wall;}
virtual Room* MakeRoom(int n) const
{return new Room(n);}
virtual Door* MakeDoor(Room* r1, Room* r2) const
{return new Door(r1, r2);}
};
上一章節中使用的函數CreateMaze函數,創建了一個小maze,有兩個房間,和一個連接這兩個房間的門。CreateMaze寫死了類的名字,使得其難以轉變成其它組件構成的maze。
這裏有一個利用MazeFactory來消除這個缺點的CreateMaze,它將MazeFactory作爲一個參數:
Maze* MazeGame::CreateMaze (MazeFactory& factory){
Maze* aMaze=factory.MakeMaze();
Room* r1=factory.MakeRoom(1);
Room* r2=factory.MakeRoom(2);
Door* aDoor=factory.MakeDoor(r1,r2);
aMaze->AddRoom(r1);
aMaze->AddRoom(r2);
r1->SetSide(North, factory.MakeWall());
r1->SetSide(East, aDoor);
r1->SetSide(South, factory.MakeWall());
r1->SetSide(West, factory.MakeWall());
r2->SetSide(North, factory.MakeWall());
r2->SetSide(East, factory.MakeWall());
r2->SetSide(South, factory.MakeWall);
r2->SetSide(West, aDoor);
return aMaze;
}
我們可以創建EnchantedMazeFactory
,一個魔法迷宮的factory,是MazeFactory的子類。EnchantedMazeFactory將會重寫不同的成員函數,同時返回不同的Room、Wall的子類。
class EnchantedMazeFactory: public MazeFactory{
public:
EnchantedMazeFactory();
virtual Room* MakeRoom(int n) const
{return new EnchantedRoom(n,CastSpell());}
virtual Door* MakeDoor(Room* r1, Room* r2) const
{return new DoorNeedingSpell(r1,r2);}
protected:
Spell* CastSpell() const;
}
假如我們想要一個可以在room放置炸彈的迷宮遊戲,如果這個炸彈爆炸了,它將會損耗牆壁。我們可以新建一個Room的子類來追蹤這個room裏是否有個炸彈和炸彈是否已經爆炸。我們也需要一個Wall的子類來追蹤牆所受傷害。我們將這兩個類稱作RoomWithABomb和BombedWall。
我們最後定義的是一個BombedMazeFactory,一個MazeFactory的子類,它可以確保wall都是BombedWall,room爲RoomWithABomb。BombedMazeFactory只需要重寫兩個函數:
Wall* BombedMazeFactory::MakeWall()const{
return new BombedWall;
}
Room* BombedMazeFactory::MakeRoom(int n)const{
return new RoomWithABomb(n);
}
要建立一個有炸彈的迷宮,我們可以簡單的使用CreateMaze,將BombedMazeFactory作爲參數傳遞給它。
MazeGame game;
BombedMazeFactory factory;
game.CreateMaze(factory);
CreateMaze也可以使用EnchantedMazeFactory來創建一個魔法迷宮。
注意到MazeFactory是一個factory方法集合,這是比較常見的實現Abstract Factory 模式的方式。這裏,MazeFactory不是一個抽象類;因此它即作爲AbstractFactory又作爲ConcreteFactory。這又是一個比較常見的實現Abstract Factory的方法。因爲MazeFactory是一個具體類,包含了所有的factory方法。創建一個新的Factory只需要設置成MazeFactory的子類,重寫一些必要的方法。
CreateMaze利用SetSide函數來指定room的邊。如果RoomWithABomb需要訪問BombedWall子類特有的成員,則需要將Wall向下投影到BombedWall。這種投影是安全的,Room中的Wall確實就是BombedWall,因爲他們都是通過BombedMazeFactory創建的。
已知的使用
Interviews使用“Kit”後綴來表示AbstractFactory類,它定義了WidgetKit和DialogKit抽象factory來生成不同觀感的UI組件。Interview也包含了一個LayoutKit,可以根據需求生成不同的組合對象。例如,一個水平layout可能根據文本方向,需要不同的組成對象。
ET++使用AbstractFactory模式來實現跨平臺(如,X Windows 和SunView)的可移植性。WindowSystem抽象基礎類定義了創建窗口組件的接口(如MakeWindow,MakeFont,MakeColor)。具體的子類根據不同的窗口系統實現相應的接口。在運行時,ET++創建一個WindowSystem子類,它會負責創建系統組件對象。
相關模式
AbstractFactory 類 通常使用Factory Method來實現,但是也可以使用Prototype來實現。一個具體的factory經常是一個Singleton。