《設計模式》之Creational模式:Builder

《設計模式》之Creational模式:Builder



目的

將複雜對象的創建和其表示相分離,因此相同的創建過程可以創造不同的表示。

驅動

一個可以變換文本格式的的RTF(Rich Text Format)閱讀器,它應該可以將RTF轉換成多種文本格式。這個閱讀器可以將RTF文檔變成ASCII文本,或者一個可以進行交互編輯的文本組件。但是,這裏有一個問題,可以轉換的文本格式種類是不定的,所以,這個閱讀器應該很容易添加其它新的轉換格式,而不需要大幅修改這個閱讀器。

有一種解決方案是給RTFReader類配置一個TextConverter對象,由這個對象將RTF轉換成爲其它的文本表現形式。RTFReader負責解析RTF文本,TextConverter來執行轉換。當RTFReader識別一個RTF token(無論是一個文字或者一個RTF控制符),它會向TextConverter發送請求來轉化這個token。TextConverter對象即負責執行文本的轉化,同時也負責將token用特定的格式顯示出來。

TextConverter 的子類具體實現了不同的轉化和格式。例如,一個ASCIIConverter將會只轉換文字符號。一個TeXConverter則需要對所有的轉化請求作出處理,這是爲了能夠產生保持所有的文本風格的TEX表示。一個 TextWidgetConverter將會產生一個複雜的用戶接口,用戶可以利用它來編輯文本。

這裏寫圖片描述

每一種converter類都採用這種方式來創建和聚集複雜的對象,同時將複雜的實現隱藏在抽象的接口下面。Converter獨立於reader,reader負責解析RTF文本。

一個Builder模式將會刻畫這種關係。每一個converter類被稱作一個builder,reader則被稱作director。對這個例子而言,Builder模式將解釋文本格式的算法(這裏就是解析RTF的方法)與如何轉換和表示分開。這樣我們就可以重用RTFReader解析算法來創建不同的文本表示形式–只需要給RTFReader配置不同的TextConverter子類。

應用

可以在以下情形使用Builder:
- 當創建一個複雜對象的算法應獨立於組成該對象的的各個部分和它們如何組成時。
- 當構建的過程必須支持不同的表現形式的被構建對象時。

結構

這裏寫圖片描述

成員

  • Builder(TextConverter)。指明創建Product對象各個部分的抽象接口。
  • ConcreteBuilder(ASCIIConverter, TeXConverter, TextWidgetConverter)。通過實現Builder的接口,構建並且彙集product的各個部分。定義和追蹤它創建的表示。提供一個取回product的接口(例如,GetASCIIText,Get-Text Widget)。
  • Director(RTFReader)。利用Builder的接口構建一個對象。
  • Product(ASCIIText,TeXText,TextWidget)。表示需要創建的複雜對象。ConcreteBuilder建造了product的內部表示,同時定義了product組建的過程。它含有定義組成部分的類,提供了將這些部分組成最後結果的接口。

合作

  • 用戶創建Director對象,同時給它配置一個想要的Builder對象。
  • Director告訴Builder何時需要創建product的某一個部分。
  • Builder處理來自director的請求,同時想product添加組成部分。
  • 用戶從builder中獲取product。

接下來的交互圖說明了Builder和Director如何與用戶合作的。
這裏寫圖片描述

結果

這裏是使用Builder模式的效果:

  1. 它讓你可以改變product的內部表示。Builder對象向director提供了構建product的抽象接口。這些接口隱藏了product的表示和內部結構,同時隱藏了product是如何被組建的。因爲product是通過一個抽象接口創建的,想要修改product的內部表示,僅僅需要定義一個新的builder。
  2. 將構建和表示代碼分離出來。通過封裝一個複雜對象的構建和表示,Builder模式改善了模塊化。用戶無需瞭解定義product內部結構的類,這些類不會在Builder的接口中出現。每一個ConcreteBuilder包含了創建和組成一個特定產品的所有代碼。代碼只需要寫一遍,不同的Director可以重用這些代碼利用相同的組成部分來創建product變體。在先前的RTF例子中,除了RTF,我們可以定義其它格式的reader,比如SGMLReader。於是,我們用相同的TextConverter來創建SGML文檔的ASCIIText,TeXText和TextWidget版本。
  3. 它爲你提供了對構建過程更細粒度的控制。不像其它的creational pattern,它們一次就可以創建一個完整的product,Builder模式則在director的指導下一步一步的構建product。只有當product完成以後,director才從builder中將它獲取出來。因此,Builder的接口比其它creational模式更能體現出構建的過程。它讓你可以更細粒度地控制構建過程和product的內部結構。

實現

一般情況下,有一個抽象Builder類來定義director想要創建的組件的操作。默認情況下,這些操作什麼也不做。ConcreteBuilder類重寫創建組件的操作。

Builder的實現需要考慮以下問題:

  1. 裝配和構建接口。Builder是一步一步的構建product,因此,Builder類的接口應該足夠一般化來應對所有具體builder的product創建。構建和裝備過程的模型成爲一個關鍵問題。一般的模型是將構建請求的結果直接添加到product即可,這種模型已經基本足夠。在RTF例子中,builder將接下來的token轉化並添加到目前已經傳化好的文本上。但是,有時你需要訪問先前構造的組成部分。在Maze例子中,MazeBuilder接口可以讓你在兩個已有的room中添加一個door。樹結構,例如解析樹是自頂向下的建造,同樣也是這個問題。在這種情況下,builder將會先返回子節點給director,director然後會將這些子節點在傳給builder來建造父節點。
  2. 爲什麼沒有product的抽象類?。一般情況下,concrete builder生產的product在表示上有着很大差異,給這些不同產品一個共同的父類並沒有什麼優勢。在RTF這個例子中,ASCIIText和TextWidget對象不可能有相同的接口,它們也不需要共同的接口。因爲用戶通常給director配置具體的builder,用戶知道正在使用哪一個具體的Builder,所以也知道最後product的具體類型。
  3. 在Builder中默認使用空函數。在C++中,構建方法一般不定義成純抽象函數,而是定義成空函數,由用戶重寫它們感興趣的方法。

代碼

我們將會定義一個CreateMaze成員函數的變體,這個函數將MazeBuilder對象作爲一個參數。

MazeBuilder類定義瞭如下接口來建造一個maze:

class MazeBuilder{
public:
    virtual void BuildMaze(){}
    virtual void BuildRoom(int room){}
    virtual void BuildDoor(int roomFrom, int roomTo){}

    virtual Maze* GetMaze(){return 0;}
protected:
    MazeBuilder();  

這些接口可以創建三個東西:1)maze,2)有着房間號的room,和3)房間之間的door。GetMaze函數會將maze返回給用戶。MazeBuilder的子類將會重寫這個函數,來返回它們建立的maze。

MazeBuilder中的建造maze的函數,默認情況下什麼也不做。這些函數沒有定義爲純的虛函數,這樣可以讓子類只重寫他們感興趣的函數。

有了MazeBuilder接口,我們可以修改CreateMaze函數,讓這個builder作爲其參數。

Maze* MazeGame::CreateMaze(MazeBuilder& builder){
    builder.BuildMaze();

    builder.BuildRoom(1);
    builder.BuildRoom(2);
    builder.BuildDoow(1,2);

    return builder.GetMaze();
}

將其與原版的CreateMaze相比較,可以發現builder是如何隱藏Maze的內部表示的,也就是隱藏room、door和wall類的定義, 還有這些組成部分如何組合在一起的。

如同其它creational patterns,Builder模式封裝了對象創建的過程,在這個例子中,是通過MazeBuilder的接口創建的。我們可以用MazeBuilder創建不同的maze。例如CreateComplexMaze函數:

Maze* MazeGame::CreateComplexMaze (MazeBuilder& builder){
    builder.BuildRoom(1);
    //...
    builder.BuildRoom(1001);
    return builder.GetMaze();
}

我們可以發現,MazeBuilder自己並不創建maze,它主要定義用來創建maze的接口。爲了方便,它只定義了一些空的函數,它的子類將會實際的實現這些函數。

子類StandarMazeBuilder用來建立簡單maze,它使用_currentMaze來保存maze的信息。

class StandardMazeBuilder : public MazeBuilder{
public:
    StandardMazeBuilder();

    virtual void BuilderMaze();
    virtual void BuilderRoom(int);
    virtual void BuilderDoor(int,int);

    virtual Maze* GetMaze();
private:
    Direction CommonWall(Room*,Room*);
    Maze* _currentMaze;
}

CommonWall用來設定兩個room之間公共wall的方向。

StandarMazeBuilder構造函數簡單的初始化_currentMaze.

StandardMazeBuilder::StandarMazeBuilder(){
    _currentMaze=0;
}

BuildMaze將會實例化一個maze,其它操作將會組建這個maze,並最終將其返回給用戶(GetMaze)。

void StandarMazeBuilder::BuilMaze(){
    _currentMaze=new Maze;
}

Maze* StandarMazeBuilder::GetMaze(){
    return _currentMaze;
}

BuildRoom函數負責創建room,並且在其周圍建立wall:

void StandarMazeBuilder::BuildRoom(int n){
    if(!_currentMaze->RoomNo(n)){
        Room* room=new Room(n);
        _currentMaze->AddRoom(room);
        room->SetSide(North, new Wall);
        room->SetSide(South, new Wall);
        room->SetSide(East, new Wall);
        room->SetSide(West, new Wall);
    }
}

爲了在兩個room間建立door,StandarMazeBuilder首先找到這兩個room,然後找到他們公共的wall:

void StandarMazeBuilder::BuildDoor(int n1, int n2){
    Room* r1 = _currentMaze->RoomNo(n1);
    Room* r2 = _currentMaze->RoomNo(n2);
    Door* d = new Door(r1,r2);
    r1->SetSide(CommonWall(r1,r2),d);
    r2->SetSide(CommonWall(r2,r1),d);
}

用戶現在可以使用CreateMaze和StandarMazeBuilder來創建一個maze:

Maze* maze;
MazeGame game;
StandarMazeBuilder builder;

game.CreateMaze(builder);
maze=builder.GetMaze();

我們本可以將所有的StandarMazeBuilder操作放在Maze中,讓Maze來建造自己。但是,減小Maze的規模可以讓它更容易理解和修改,並且StandarMazeBuilder很容易從Maze中分離出來。更重要的是,將這兩者分離,可以讓你有多樣的MazeBuilder,沒一種MazeBuilder使用不同的room、wall和door類。

一個比較特殊的MazeBuilder是CountingMazeBuilder。這個builder不會創建maze,它只統計已經被創建的各種組件。

class CountingMazeBuilder:public MazeBuilder{
public:
    CountingMazeBuilder();

    virtual void BuildMaze();
    virtual void BuildRoom();
    virtual void BuildDoor(int,int);
    virtual void AddWall(int, Direction);

    void GetCounts(int&, int&)const;
private:
    int _doors;
    int _rooms;
};

構造函數會初始化計數器,重寫的MazeBuilder操作將會增加這些計數器。

CountingMazeBuilder::CountingMazeBuilder(){
    _rooms=_doors=0;
}

void CountingMazeBuilder::BuildRoom(int){
    _rooms++;
}

void CountingMazeBuilder::BuildDoor(int,int){
    _doors++;
}

void CountingMazeBuilder::GetCounts(
    int& rooms, int& doors
)const{
    rooms=_rooms;
    doors=_doors;
}

用戶可能如下使用CountingMazeBuilder:

int rooms,doors;
MazeGame game;
CountingMazeBuilder builder;

game.CreateMaze(builder);
builder.GetCount(rooms,doors);

cout<<"The maze has"<<rooms<<" rooms and "<<doors<<" doors"<<endl;

已知的使用

來自ET++的RTF轉化應用,它的文本建立模塊使用一個builder來處理以RTF格式存儲的文本。

在Smalltalk-80中,Builder是一個很常見的模型:

  • 在編譯器子系統中的Parser類,是一個Director,它將ProgramNodeBuilder作爲一個參數。每當Parser識別出一個語法結構,它會通知ProgramNodeBuilder。當解析完畢,它會向builder索要解析樹,然後將其返回給用戶。
  • ClassBuilder是一個builder,Classes用它來創建它自己的子類。這時,一個Class既是Director,也是Product。
  • ByteCodeStream是一個builder,它可以比特組的方式創建編譯過得方法。ByteCodeStream並不是標準的Builder模式,因爲它要創建的複雜對象被編碼爲比特數組形式,而不是一個正常的Smalltalk對象。但是ByteCodeStream的接口卻是典型的builder接口,並且很容易用其它的類替換ByteCodeStream,只要這個類可以將程序表示成組合對象的形式。

Adaptive Communications Environment中的Service Configurator 框架使用一個builder來構建網絡服務組件,這些組件會在運行時連接到服務器。這些組件通過一個配置語言來描述,這個語言可以被LALR解析。解析器的語法行爲將會利用builder的操作向服務器組件添加信息。在這裏,解析器就是Director。

相關模式

Abstract Factory和Builder很相似,它們大多情況下來創建複雜對象。基本不同是Builder模式注重於一步一步創建複雜的對象。Abstract Factory更強調一個家族的product對象(可以簡單也可複雜)。Builder在最後一步時獲得product,但是Abstract Factory會立即得到想要的product。

Composite經常利用builder來構建。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章