天天看點

設計模式之工廠方法(Factory Method)

一 目的

    定義一個建立對象的接口,但是讓他的子類去決定初始化哪種類型。工廠方法使得一個類能夠推遲到他的子類去初始化。

二 動機

    架構運用抽象類來定義和維護對象之間的關系。一個架構經常負責這些對象的建立。考慮一些這麼一個情況:一個能夠展現多個文檔的應用程式的架構。在這個架構中有兩個關鍵的抽象,一個是應用程式,一個是文檔。兩個類都是抽象的,用戶端代碼必須子類化他們,進而完成特定程式的實作。例如,去建立一個畫圖應用程式,我們會定義類似 DrawingApplication 和 DrawingDocument 這兩個類。這個應用程式類負責管理文檔,當有請求的時候,并且建立他們,比如當使用者選擇打開或者建立菜單時。

    因為這個需要初始化的特别的文檔子類是由應用程式指定的,是以 Application 這個類不能夠指定哪種文檔的子類需要初始化。這個 Applicaiton 類隻知道什麼時候去初始化一個文檔,但是不知道是什麼種類的文檔。這裡有一個困境:架構必須初始化類,但是他隻知道一些它不能夠初始化的抽象類。

    工程方法提供了一個解決方法。他封裝了哪個文檔子類應該建立的資訊,并且把這些資訊從架構中移出去。

設計模式之工廠方法(Factory Method)

Application 的子類重新定義了CreateDocument這個接口,傳回一個合适的文檔子類對象。一旦一個Application 的子類被初始化,他就可以在不知道他們具體哪個類的情況下,初始化應用程式特定的文檔。我們稱CreateDocument為一個工程方法,因為它負責建立一個對象。

三 應用

    當有以下情況的時候,可以考慮使用工廠方法;

    1 一個類不能夠預期知道它必須建立的對象的類;

    2 一個類想讓它的子類來指定它需要建立的對象;

    3 有些類需要委托一些職責給他的一個幫助類型的子類,然後你想本地化那些被委托的幫助子類的資訊。

四 結構

設計模式之工廠方法(Factory Method)

五 參與者

    product(Document)

    定義了工廠方法建立的對象的接口。

    ConCreteProduct(MyDocument)

    實作了 Product 的接口。

    Creator(Application)

    -- 聲明了傳回産品對象的工廠方法。建立者可以定義一個預設的工廠方法。這個工廠方法傳回一個預設的具體産品對象。

    -- 可以調用工廠方法來建立一個産品對象。

    ConCreteCreator(MyApplication)

    --重寫了工廠方法,傳回一個具體産品對象的執行個體。

六 合作

    建立者依賴它的子類來定義工廠方法,進而使它能夠傳回一個合适的具體産品的執行個體。

七 影響

    工廠方法忽略了把特定程式的類綁定到你的代碼中的需求。代碼隻是處理産品接口,是以它能夠和任何使用者定義的類型一起工作。

    一個潛在的缺點是為了建立一個特定的具體産品對象,用戶端代碼必須子類化建立者類。不管怎樣,子類化在用戶端必須要初始化建立者類的時候是很好的,但是用戶端代碼必須處理另外的一些評估事項。

      以下是兩個工廠方法模式額外的後果。

       1 為子類提供鈎子。用工廠方法在一個類中建立一個對象總是比直接建立一個對象更加靈活。工廠方法給子類一個鈎子用來提供建立對象的可擴充版本。

         在文檔的例子中,文檔類能夠定義一個叫做CreateFileDialog的工廠方法,來建立一個為了預設的檔案對話框用來打開一個已經存在的文檔。一個文檔的子類能夠重新定義特定程式的檔案對話框。在這種情況下,工廠方法不是抽象的,它可以提供一個合理的預設實作。

        2 連接配接并行的類層次。在我們目前所考慮的例子中,工廠方法隻被建立者調用。但這個并不是固定一成不變的。用戶端代碼會發現工廠方法很有用,特别是在并行的類層次中。

        當一個類委托一些它自己的職責給一個單獨的類時會産生并行的類層次。考慮到一個圖形資料,它們可以被我們互動操作,也就是,他們可以被我們用滑鼠拉伸,移動,旋轉。實作這些互動不不是很容易。它常常要求我們存儲和更新資訊。這些資訊記錄了某個時刻的操作狀态。是以,它并不需要被儲存在這些圖像對象中。更重要的是,當使用者操作他們時,不同的圖像的行為各異。例如,拉伸一條直線會産生移動末端節點的效果,二拉伸一個文本圖形會改變它所占的行空間。

    在這些限制下,用一個操作對象來實作互動和跟蹤任何特定操作狀态會更加好。不同的圖形會用不同的操作對象的子類去處理某種特别的互動。那麼操作結果的類層次和圖形類層次并行,如下圖:

設計模式之工廠方法(Factory Method)

    這個圖形類提供了一個叫 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());

    }

}

繼續閱讀