設計模式之工廠方法(Factory Method)
一 目的
定義一個建立物件的介面,但是讓他的子類去決定初始化哪種型別。工廠方法使得一個類能夠推遲到他的子類去初始化。
二 動機
框架運用抽象類來定義和維護物件之間的關係。一個框架經常負責這些物件的建立。考慮一些這麼一個情況:一個能夠展現多個文件的應用程式的框架。在這個框架中有兩個關鍵的抽象,一個是應用程式,一個是文件。兩個類都是抽象的,客戶端程式碼必須子類化他們,從而完成特定程式的實現。例如,去建立一個畫圖應用程式,我們會定義類似 DrawingApplication 和 DrawingDocument 這兩個類。這個應用程式類負責管理文件,當有請求的時候,並且建立他們,比如當用戶選擇開啟或者新建選單時。
因為這個需要初始化的特別的文件子類是由應用程式指定的,所以 Application 這個類不能夠指定哪種文件的子類需要初始化。這個 Applicaiton 類只知道什麼時候去初始化一個文件,但是不知道是什麼種類的文件。這裡有一個困境:框架必須初始化類,但是他只知道一些它不能夠初始化的抽象類。
工程方法提供了一個解決方法。他封裝了哪個文件子類應該建立的資訊,並且把這些資訊從框架中移出去。
Application 的子類重新定義了CreateDocument這個介面,返回一個合適的文件子類物件。一旦一個Application 的子類被初始化,他就可以在不知道他們具體哪個類的情況下,初始化應用程式特定的文件。我們稱CreateDocument為一個工程方法,因為它負責建立一個物件。
三 應用
當有以下情況的時候,可以考慮使用工廠方法;
1 一個類不能夠預期知道它必須建立的物件的類;
2 一個類想讓它的子類來指定它需要建立的物件;
3 有些類需要委託一些職責給他的一個幫助型別的子類,然後你想本地化那些被委託的幫助子類的資訊。
四 結構
五 參與者
product(Document)
定義了工廠方法建立的物件的介面。
ConCreteProduct(MyDocument)
實現了 Product 的介面。
Creator(Application)
-- 聲明瞭返回產品物件的工廠方法。建立者可以定義一個預設的工廠方法。這個工廠方法返回一個預設的具體產品物件。
-- 可以呼叫工廠方法來建立一個產品物件。
ConCreteCreator(MyApplication)
--重寫了工廠方法,返回一個具體產品物件的例項。
六 合作
建立者依賴它的子類來定義工廠方法,從而使它能夠返回一個合適的具體產品的例項。
七 影響
工廠方法忽略了把特定程式的類繫結到你的程式碼中的需求。程式碼只是處理產品介面,因此它能夠和任何使用者定義的型別一起工作。
一個潛在的缺點是為了建立一個特定的具體產品物件,客戶端程式碼必須子類化建立者類。不管怎樣,子類化在客戶端必須要初始化建立者類的時候是很好的,但是客戶端程式碼必須處理另外的一些評估事項。
以下是兩個工廠方法模式額外的後果。
1 為子類提供鉤子。用工廠方法在一個類中建立一個物件總是比直接建立一個物件更加靈活。工廠方法給子類一個鉤子用來提供建立物件的可擴充套件版本。
在文件的例子中,文件類能夠定義一個叫做CreateFileDialog的工廠方法,來建立一個為了預設的檔案對話方塊用來開啟一個已經存在的文件。一個文件的子類能夠重新定義特定程式的檔案對話方塊。在這種情況下,工廠方法不是抽象的,它可以提供一個合理的預設實現。
2 連線並行的類層次。在我們目前所考慮的例子中,工廠方法只被建立者呼叫。但這個並不是固定一成不變的。客戶端程式碼會發現工廠方法很有用,特別是在並行的類層次中。
當一個類委託一些它自己的職責給一個單獨的類時會產生並行的類層次。考慮到一個圖形資料,它們可以被我們互動操作,也就是,他們可以被我們用滑鼠拉伸,移動,旋轉。實現這些互動不不是很容易。它常常要求我們儲存和更新資訊。這些資訊記錄了某個時刻的操作狀態。因此,它並不需要被儲存在這些影象物件中。更重要的是,當用戶操作他們時,不同的影象的行為各異。例如,拉伸一條直線會產生移動末端節點的效果,二拉伸一個文字圖形會改變它所佔的行空間。
在這些限制下,用一個操作物件來實現互動和跟蹤任何特定操作狀態會更加好。不同的圖形會用不同的操作物件的子類去處理某種特別的互動。那麼操作結果的類層次和圖形類層次並行,如下圖:
這個圖形類提供了一個叫 CreateManipulator的工廠方法,它能讓客戶端程式碼建立一個和圖形相對應的操作物件。圖形子類重寫這個工廠方法,返回一個與他們相對應的操作物件的例項回來。另外,這個圖形類可以實現CreateManipulator,返回一個預設的操作物件例項,圖形子類可以簡單的繼承這個預設實現。這些圖形類並不一定要相應的操作子類,因為這個層次可以部分並行。
我們需要注意工廠方法是怎樣去把這兩個類的關係連線起來的。它保留了哪些類需要放在一起的資訊。
八 實現
當應用工廠方法時,需要考慮以下幾個問題。
1 兩個主要的變種。(1)一種情況是當這個建立者類是一個抽象類,並且不提供它宣告的工廠方法的實現(2)第二種情況是當這個建立者是一個具體的類,並且提供了它宣告的工廠方法的預設實現。也有可能抽象類中提供了工廠方法的實現,但是這個並不常見。
第一種情況要求子類去定義一個實現,因為沒有一個合理的實現。這樣逃避了不得不初始化不可預見的類的困境。第二種情況,具體的建立者為了靈活性而優先使用工廠方法。它遵循如下規則:“在一個單獨的操作中建立一個物件,這樣可以使得它的子類能夠重寫它”。這個規則保證了在需要的情況下,子類的設計者可以改變他們的父類初始的化的物件。
2 引數化工廠方法。另外一個工廠方法的版本是讓工廠方法建立多種產品。工廠方法的引數標示了將要被建立物件的種類。所有這個工廠方法要建立的物件共享一個產品介面。在文件的例子中,Application 會支援不同種類的文件。你傳入一個引數給CreateDocument 用來指定要建立的文件的種類。
一個有引數的工廠方法一般有如下的形式,MyProduct 和 YourProduct 是 Pruduct 的子類。
package com.hermeslch.pattern;
enum ProductId {
MINE,YOURS,THEIRS
}
public class Creator {
public Product Create(ProductId id){
if(id == ProductId.MINE) return new MyProduct();
if(id == ProductId.YOURS) return new YourProduct();
return null;
}
}
重寫一個含參的工廠方法使得你能偶輕易和有選擇性的擴充套件或者改變建立者建立的產品。你可以為新的產品增加新的識別符號,或者你能夠用已經存在的識別符號建立不同的產品,如下程式碼:
class MyCreator{
public Product Create(ProductId id){
if(id == ProductId.MINE) return new YourProduct();
if(id == ProductId.YOURS) return new MyProduct();
if(id == ProductId.THEIRS) return new TheirProduct();
return Creator::Create(id);
}
}
注意到最後一步操作是呼叫父類的Create()。這是因為MyCreator::Creator 只處理 YOURS,MINE,THEIR,只有這三種是和父類不相同的,它對其他的型別不感興趣。因此MyCreator 擴充套件了建立的型別。它延遲了建立所有種類的產品,只是建立一些。
3 由於實現語言的不同,會導致工廠方法有不同的變種和其他問題。這裡不做詳細翻譯和描述了。比如,java,C++,SmallTalk 的實現方法有些稍微的區別。
4 可以使用模板來避免繼承。我們提到過,另一個工廠方法潛在的問題是為了建立一個合適的產品物件,會使你不得不子類化。另外一個避免這個問題的方式是提供一個模板給Creator的子類。
5 名字約定。使用名字約定可以使得你明確表示你在使用工廠方法。比如MacApp 總是這樣寫函式 Class doMakeClass(),這個Class 就是產品的類名。
九 例子程式碼
函式 CreateMaze() 建立和返回一個迷宮。有一個問題就是這個函式裡面硬編碼了Maze,rooms,doors和其他的walls.我們這裡介紹工廠方法,使得子類來選擇這些元件。
首先,我們將在MazeGame中定義工廠方法來建立maze,room,wall,和door這些物件。
package com.hermeslch.pattern;
import org.junit.Test;
public class MazeGame {
public Maze makeMaze(){
return new Maze();
}
public Room makeRoom(int index){
return new Room(index);
}
public Wall makeWall(){
return new Wall();
}
public Door makeDoor(Room r1,Room r2){
return new Door(r1,r2);
}
}
每個工廠方法返回一個給定的迷宮元件。MazeGame提供一個預設的實現。他們返回牆壁,迷宮,門,房間的最簡單的實現。現在我們可以重新寫CreateMaze來使用這些工廠方法。
public Maze CreateMaze(MazeFactory factory){
Maze maze = makeMaze();
Room r1 = makeRoom(1);
Room r2 = makeRoom(2);
Door aDoor = makeDoor(r1, r2);
maze.AddRoom(r1);
maze.AddRoom(r2);
r1.setSide(Direction.North,makeWall());
r1.setSide(Direction.East,aDoor);
r1.setSide(Direction.South,makeWall());
r1.setSide(Direction.West,makeWall());
r2.setSide(Direction.North,makeWall());
r2.setSide(Direction.East,makeWall());
r2.setSide(Direction.South,makeWall());
r2.setSide(Direction.West,aDoor);
return maze;
}
不同的遊戲可以子類化MazeGame ,從而來指定迷宮的一些區域性特點。MazeGame的子類可以重新定義一些或者所有的工廠方法,並且指定某種特別的產品變種。例如:一個BomedMazeGame 能夠重新定義房間和牆壁產品物件。
Class BombedMazeGame : public MazeGame{
public BomedMazeGame();
public Wall MakeWall(){
return new BombedWall();
}
public Wall MakeRoom(int n){
return new BombedRoom(n);
}
一個 EnchantedMazeGame 也許會定義成這樣:
public class EnchantedMazeGame extends MazeGame{
public EnchantedMazeGame();
public Wall MakeDoor(Room r1,Room r2){
return new DoorNeedingSpell(r1,r2);
}
public Wall MakeRoom(int n){
return new EnchantedRoom(n,new CastSpell());
}