1.2.1 设计模式:Builder——建造者模式

Intent(意图)

将一个复杂对象的构建(construction)与其表示(representation)分离,这样同样的构建过程可以创建出不同的表示。

Motivation(动机)

一个RTF(Rich Text Format)文档转换格式的阅读器必须可以将RTF转换成多种文本格式。该阅读器必须能将RTF文档转换成无格式的ASCII文本,或者转换为可交互编辑的文本插件(text widget)。但问题是,转换的可能性种类是开放的。因此必须能在不修改阅读器的前提下添加新的转换。

解决方法之一是使用一个TextConverter对象配置RTFReader类,该对象可以将RTF转换为另一种文本表示。当RTFReader传入RTF文档时,它使用TextConverter执行转换。一旦RTFReader识别出一个RTF token(RTF标记,纯文本或者RTF控制字(RTF control word)),它向TextConverter发布一个转换该token的请求。TextConverter对象负责执行数据转换和以特定格式表示这个token.

TextConverter的子类根据不同的转换和格式区分开来。例如ASCIIConverter忽略纯文本以外的任何请求。而TeXConverter接受所有的请求从而在文本中生成包含所有风格的TeX表示。TextWidgetConverter将构建一个复杂的user interface(用户界面)对象,使得用户可以阅读和编辑文本。

译者注:上文说的是一种多格式文本,类似HTML文件,根据不同的标签,将文本表示成不同形式。这里也是根据不同标记使用不同的转换器转换文本格式。

image

每种转换器类都为创建和装配一个复杂对象提供机制,并将其隐藏在一个抽象接口中。转换器与阅读器隔离,阅读器负责解析(parsing)RTF文档。

Buider模式满足(captures)所有这些关系。在该模式中,每一个converter(转换器)类称为一个Builder,而这个阅读器称为Director(指挥者)。应用到这个例子中,Builder模式将翻译文本格式(也就是RTF文档的解析)这个逻辑从怎样创建和表示转换后格式中分离出来。这使得我们可以复用RTFReader的解析逻辑从RTF文档创建出不同的文本表示——只需使用不同的TextConverter子类配置RTFReader

Applicability(应用)

以下情况使用Builder模式:

  • 创建复杂对象的逻辑应该独立于该对象的组成部分以及他们的组成方式(how they’re assembled)。
  • 构造过程必须允许被构造对象有不同的表示

Structure(结构)

image

Participants(参与者)

  • Builder(TextConverter)
    • 指定一个创建Product对象的部件(part)的抽象接口
  • ConcreteBuilder(ASCIIConverter,TeXConverter,TextWidgetConverter)
    • 通过实现Builder接口构造(constructs)和组装(assembles)产品部件
    • 定义并保存(keep track of)它创建的表示。
    • 提供获取(retrieving)产品的接口(例如GetASCIIText,GetTextWidget)
  • Director(RTFReader)
    • 使用Builder接口构建对象。
  • Product(ASCIIText, TeXText, TextWidget)
    • 表示正在构造的复杂对象。ConcreteBuilder建造了产品(product)的内部表示,通过ConcreteBuilder定义的方法组装成了它(译者注:指product)。
    • 含有定义组成部分(constituent parts)的类,含有将部件(parts)组装成最终结果的接口。

Collaborations(协作)

  • 客户端创建Director对象并使用想要(desired)的Builder对象配置它(译者注:它指Director)。
  • 每当需要建造product的一个部件时Director就通知Builder
  • Builder处理来自Director的请求并向product中添加part
  • 客户端从Builder中获取产品。

下面的交互图阐述了BuilderDirector如何与客户端协作。
image

Consequences(结果)

这里是Builder模式的结果:

  1. 它使得你可以改变产品的内部表示。Builder对象为director提供一个构建product的抽象接口。该接口允许builder隐藏产品的表示及其内部结构。它同时也隐藏了产品的组装方式。因为产品是通过一个抽象接口构建,你要更改产品的内部表示只需定义一个新的builder.
  2. 它隔离了构建(construction)代码和表示(representation)代码。Buider模式通过封装复杂对象的构建方式和表示方式提升了模块性。客户端无需知道有关定义product内部结构的类的任何信息;这样的类不会出现在Builder的接口中。

    每个ConcreteBuilder包含了创建和组装特定种类的product的所有代码。代码只编写一次,之后不同的Director可以复用它从同样的部件组中(the same set of parts)建造Product变体。在先前的RTF例中,我们可以为RTF之外的格式定义一个阅读器,叫SGMLReader,然后使用同样的TextConverter去产生SGML文档的ASCIITextTeXTextTextWidget译文。

  3. 它让你对构建过程有更精细的控制。不像一步到位地构造product的创建模式,Builder模式在director的控制下,一步一步地构建product.只有当product完成了,director才会从builder中获取它。因此相比其他创建型模式,Builder接口更能反映product的构建过程。这使得你对构建过程有更精细的控制,并由此控制最终的(resulting)product的内部结构。

Implementation(实现)

通常,有一个抽象的Builder类为每一个组件定义一个操作,director会用这个操作请求创建该组件。该操作默认情况下不做任何事。ConcreteBuilder类根据它要创建的组件覆盖该操作。

这里是其他需要考虑的实现问题:
1. 组装和构造接口。Builder通过一步一步的方式构建他们的product.因此Builder类接口必须足够全面以让所有的concrete builder都可以构造product

一个关键设计问题涉及到构建和组装过程的模型。将构造请求结果简单附加到`product`这种模型通常是足够的。在RTF实例中,`builder`转换下一个`token`并将其附加到它目前已转换的文本中。

但是有时候你可能需要访问早期构造的`product`的部分。在实例代码的`Maze`(迷宫)示例中,`MazeBuilder`接口让你在已存在的房子之间添加一扇门。像自底向上构建的解析树这样的树结构也是如此。在那种情形中,`builder`要返回孩子节点给`director`,随后`director`将它们回传给`builder`从而建立父节点。
  1. 为何product没有抽象类?在通常情形下,经concrete builder产生的product在表示上有很大差别以至于给不同的product定义一个公有的父类没有任何好处。在RTF实例中,ASCIITextTextWidget对象不可能有也不需要一个公有的接口。因为客户端通常会使用合适的concrete builder配置director,客户端只需要知道Builder哪一个concrete子类在用,并对其product进行相应处理.
  2. Builder中的方法默认为空实现。在C++中,build方法有意不声明为纯虚拟成员函数,而是定义为空方法,让客户端只覆盖他们感兴趣的操作。

Sample Code(示例代码)

我们将定义多种以MazeBuilder类的一个builder(对象)为参数的CreateMaze成员函数。
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)编了房号的房子之间的门。GetMaze操作返回mazeclient. MazeBuilder的子类将覆盖该操作返回他们建造的maze.

MazeBuilder所有的maze-building操作默认不做任何事。他们没有声明为纯虚拟,以使得派生类只覆盖他们感兴趣的方法。

给定了MazeBuilder接口,我们可以修改CreateMaze成员函数以将该builder作为参数。

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

    builder.BuildRoom(1);
    builder.BuildRoom(2);
    builder.BuildDoor(1, 2);
    return builder.GetMaze();
}

将该版本的CreateMaze与原版对比。注意builder是如何隐藏Maze的——即定义room,doorwall的这些类,以及这些部分是如何组装完成最终的maze。某些人可能会想到这里有表示roomdoor的类,但是并没有看到表示wall的类(but
there is no hint of one for walls.)。这是为了让更改maze的表示方式更加容易,这样所有MazeBuilderclient都不需要更改。

与其他创建型模式一样,Builder模式封装了对象的创建方式(how objects get created),本例中是通过MazeBuilder定义的接口。这意味着我们可以复用MazeBuilder建造不同种的mazes.CreateComplexMaze操作给出了一个示例:

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

    return builder.GetMaze();
}

注意MazeBuilder本身并不会创建maze;它的主要目的只是定义一个创建maze的接口。为了方便,它主要定义了空实现。MazeBuilder的子类做实际的工作。

子类StandardMazeBuilder是一个建造简单maze的实现。它将正在创建的maze存放在变量_currentMaze中。

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

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

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

ConmonWall是决定两个room之间的公共wall的方向的一个工具操作。
StandardMazeBuilder构造器简单地初始化_currentMaze.

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

BuildMaze实例化一个Maze的同时其他操作会组装并最终返回给client(使用GetMaze)。

void StandardMazeBuilder::BuildMaze () {
    _currentMaze = new Maze;
}
Maze* StandardMazeBuilder::GetMaze () {
    return _currentMaze;
}

BuildRoom操作创建一个room并建造其周围的wall:

void StandardMazeBuilder::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);
    }
}

要在两个房间之间创建一扇门,StandardMazeBuilder查看maze中的这两间房并找到它们的相邻的墙:

void StandardMazeBuilder::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);

现在客户端可以结合StandardMazeBuilder使用CreateMaze创建一个maze:

Maze* maze;
MazeGame game;
StandardMazeBuilder builder;

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

我们可以将所有StandardMazeBuilder操作放到Maze中并让每一个Maze建造自身。但是Maze的轻量化有助于更容易理解和修改Maze,并且让StandardMazeBuilder容易从Maze中分离。最重要地,将两者分离可以让你有多种MazeBuilder,每一种使用不同的roomwalldoor类。

CountingMazeBuilder是一个更加特殊(exotic)的MazeBuilder.该builder根本不会创建maze;他只是记录将要创建的不同种类的组件的数目。

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

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

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

构造器初始化counters,并覆盖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;
}

这里是client使用CoutingMazeBuilder的方式:

int rooms, doors;
MazeGame game;
CountingMazeBuilder builder;

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

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

Known Uses(熟知的应用)

RTF转换器应用来自ET++ [WGM88]. 它的文本建造块使用builder执行以RTF格式的文本存储。
在Smalltalk-80 [Par90]中,Builder是一个常用的模式:
+ 编译器子系统中的解析类是一个以ProgramNodeBuilder对象作为参数的Director.解析对象每次识别出一个语法结构时通知ProgramNodeBuilder对象。当解析完成,它向builder请求它建造的解析树并将其返回给client.
+ ClassBuilder是Classes用来为自身创建子类的builder.在该例子中一个Class既是Director又是Product.
+ ByteCodeStream是一个以byte数组的形式创建编译后方法的builder.ByteCodeStream是对Builder模式的非标准应用,因为它建造的复杂对象被编码成一个byte数组,而不是一个正常的SmallTalk对象。但是ByteCodeStream的接口是一个典型的builder,并且我们很容易使用一个将程序表示为复合对象的不同类代替ByteCodeStream.

来自Adaptive Communications Environment的服务配置框架使用builder构造运行时连接服务器的网络服务组件。该组件的描述使用经LALR解析器解析的配置型语言。该解析器的语义行为执行该builder上的操作,将信息添加到服务组件。在该例中,解析器是个Director.

抽象工厂也可以构建复杂对象,这一点与Builder类似。主要的不同在于Builder模式侧重于一步一步地构建复杂对象。而抽象工厂的重点在于product对象(或简单或复杂)的系列(families)。Builder是在最后一步返回product,但就抽象工厂而言,product是立即返回的。
builder建造的通常是一个Composite

猜你喜欢

转载自blog.csdn.net/kevinscsdn/article/details/79090512