《设计模式》之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来构建。

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