一 目的:
把一个复杂对象的创建过程和其展现过程分离开来,从而使相同的创建过程能够创造不同的展现。
二 动机:
一个阅读器要求能够将富文本格式的文档转换成很多文本格式。阅读器可能将富文本文档转换成普通的Ascii 文本,或者转换成可以编辑和交互的文本窗口。这里存在一个问题,这种可能的转换数量是开放的。所以要求我们在增加一种新的转换方式的时候尽量简单,不能够去修改阅读器。
其中有一个解决方案是在这个阅读器(RTFReader)上配置一个能够将富文本格式转换成其他的文本的文本转换器(TextConverter)。当阅读器在解析这个富文档的时候,它调用文本转换器去转换。当阅读器识别出富文本的种类后,它分别去调用文本转换器的不同接口去进行处理。文本转换器对象负责数据转换和将内容以特定的形式展现出来。
文本转换器的子类指定了不同的转换方法和展现格式。例如:一个 ASCIIConverter 忽略除普通文字的其他任何格式。一个TexConverter,相反,为了创建一个Tex展现格式,能够获取所有格式信息的,会实现所有请求的接口。一个TextWidgetConverter 会创建一个复杂的用户接口,从而使用户能够看到和编辑这些文本。
每个转换器都会有自己创建和装配复杂对象的机制,并且把自己放在接口的后面。转换器和负责解析富文本文档的阅读器进行分离。
建筑者模式考虑了所有的这些关系。在整个模式中,每种转换器被称作建筑者。阅读器被称为主管。在这个例子中,建筑者模式把解析文本形式的算法和转换后的文本的创建和展示分离开来。这个可以使我们能够重复使用阅读器解析富文本文档的算法,从而使我们只要把阅读器配置成不同的转换器,就能够创建不同的文本展现。
三 应用:
我们可以在以下场景中使用建筑者模式:
1 创建复杂对象的算法 和 组成对象的局部以及他们怎么装配的过程 需要保持独立的情况;
2 被创建的对象的创建过程必须允许不同形式的展现。
四 结构:
五 参与者:
. 建筑者(TextConverter)
声明了创建产品的不同部分的各种接口。
. 具体创建者(ASCIIConverter,TexConverter,TextWidgetConverter)
通过实现接口来创建和装配产品的部分。
定义和跟踪他创建对象的展现。
提供一个获取他创建的产品的接口。
. 主管 (Director)
通过建筑者的接口创建一个对象。
. 产品
展示被创建的对象。建筑者创建产品的内部展现并且定义了他装配的过程。
包括构成这个产品的局部的类,并且包括用来完成装配这个产品的局部的接口 。
六 合作
用户代码创建主管对象,并且用设计好的建筑者配置好。
当产品的部分需要被创建的时候,主管通知建筑者。
建筑者响应主管的请求,并且把部分添加到产品中。
用户代码从建筑者中获取到产品。
下面是时序图:
七 影响:
下面列举了建筑者模式的主要影响
1 建筑者模式使你可以改变产品的内部展现。建筑者给主管提供了一个创建产品的抽象接口。接口使得建筑者能够隐藏产品的展现和内部结构。它同时也隐藏了这个产品是如何装配的。因为这个产品是通过接口创建的。如果你想改变一个产品的内容展现,你必须做的是定义一个新的建筑者。
2 建筑者模式隔离了创建过程和展示。建筑者模式通过封装复杂对象的创建方式提高了模块化程度。客户代码不需要知道定义产品内部结构的类的任何东西。这些类不会出现在建筑者的接口中。每个具体的建筑者包含了创建和装配某个具体类型的产品的所有代码。这些代码只写一次,主管可以重复使用他们来创建相同一组的产品变种。在早期的富文本格式例子中,我们其实还可以定义其他的阅读器,比如说标准通用标示语言阅读器,然后用同样的文本转换器(TextConverter)来产生标准通用标示语言的展现形式,比如ASCIIText,TexText,以及TextWidget。
3 它使你能够在创建的过程中更好的 控制。不像其他的创建模式一样,一次性创建产品,建筑者模式在主管的控制下一步一步的创建产品。只有当这些产品全部创建好了,主管才能够通过建筑者来获取到这个产品。因此,建筑者模式的接口比其他创建模式更多的反映了创建过程。这个给你在创建产品和决定最终结果的内部结构更多的控制。
八 实现
有一个抽象的建筑者类来定义创建主管请求创建的组件的操作。这些操作默认什么事情也不做。一个具体的建筑者类会为自己感兴趣的组件去重写这些操作。
这里还有其他一些事项问题需要考虑:
1 装配和创建的接口。创建者一步一步的创建这些产品。因此,这个创建者的接口必须足够,覆盖所有的具体实现类的操作。
在设计中,需要考虑一个关键的问题就是创建和装配的模型。把创建请求的结果简单的追加到产品的模型通常是有效的。在富文本例子中,建筑者转换和追加了下一个内容到之前他已经创建的产品上。但是有些时候,他也许需要访问早起创建的部分产品。。在迷宫的例子中,我们展现的例子代码,迷宫建筑者的接口让你在存在的房间中增加一扇门。树形结构,比如从下到上创建的分析树是另一个例子。在那种情况下,建筑者会返回子节点给主管。主管然后把他们又传给建筑者来创建复节点。
2 为什么没有产品的抽象类?在一般的情况下,被具体创建者创建的产品在展现时非常不同,所以给不同的产品一个共同的接口得不偿失。在符文本文档中,ASCIIText 和TextWidget 对象没有共同的接口,所以他们不需要。因为客户端代码总是给主管配置合适的具体创建者,用户可以知道哪个具体的创建者在被使用,从而能够相应的处理它的产品。
3 在建筑者的接口方法默认为空。在C++中,创建方法设计成不声明为虚函数。他们被定义为空函数,从而使客户重载他们感兴趣的操作。
九 例子代码
我们会定义一个CreateMaze 成员函数的变种。它的一个参数是 类 MazeBuilder。
这个MazeBuilder类定义了以下接口:
package com.hermeslch.pattern;
public interface MazeBuilder {
public void BuildMaze();
public void BuildRoom(int room);
public void BuildDoor(int roomFrom,int roomTo);
public Maze GetMaze();
}
这个接口能够创建三件东西:(1)迷宫 (2)有一个数字标识的房间 (3)被标识的房间之间的门。GetMaze操作把迷宫返回给客户端代码。MazeBuilder的子类会重写这些操作,从而能够返回他们创建的迷宫。
所有的创建迷宫的操作默认不做任何事情。
把MazeBuilder做为参数,我们能够改变CreateMaze成员函数,把MazeBuilder作为一个参数。 public Maze CreateMaze(MazeBuilder builder){
builder.BuildMaze();
builder.BuildRoom(1);
builder.BuildRoom(2);
builder.BuildDoor(1,2);
return builder.GetMaze();
}
和原来CreateMaze的版本进行比较,注意到建筑者模式隐藏了迷宫的内部展现,即定义房间,门和墙的类,以及这些部分怎么组装成最终的迷宫。有些人会注意到房间和门是有定义的类,但是没有墙的定义。这样使得我们很容易改变这个迷宫的展现形式,因为MazeBuilder 的客户端代码都没有改变。
像其他的创建模式一样,建筑者模式通过定义的接口隐藏了对象是怎样创建的。这就意味着我们可以重复使用MazeBuilder来创建不同的迷宫。CreateComplexMaze操作给了一个很好的例子:
public Maze CreateComplexMaze(MazeBuilder builder){
builder.BuildMaze();
builder.BuildRoom(1);
//...
builder.BuildRoom(100);
return builder.GetMaze();
}
注意到MazeBuilder自己不创建迷宫,他的目的只是定义一个创建迷宫的接口。为了方遍,它事先创建一个空的实现。MazeBuilder的子类来做实际的工作。
子类StandarMazeBuilder是一个创建简单迷宫的实现。他用 成员变量 _currentMaze记录了他创建的迷宫。CommonWall是一个决定普通的两个房间的墙壁方向的通用的操作。
这个 StanderMazeBuilder 的构造器简单的初始化_currentMaze.
public StandarMazeBuilder(){
_currentMaze = null;
}
BuildMaze 初始化一个迷宫,其他的操作来组装,把最终结果返回给客户端。
public void buildMaze(){
_currentMaze = new Maze();
}
public Maze getMaze(){
return _currentMaze;
}
buildRoom 操作创建一个房间,并且创建它周围的墙壁。
public void buildRoom(int n){
if(_currentMaze.roomNo(n) != null){
Room room = new Room(n);
_currentMaze.AddRoom(room);
room.setSide(Direction.North, new Wall());
room.setSide(Direction.South, new Wall());
room.setSide(Direction.East, new Wall());
room.setSide(Direction.West, new Wall());
}
}
为了创建两个房间中的门,StanderMazeBuilder寻找迷宫里面的两个房间,然后找到他们临近的墙壁。
public void 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,结合StanderMazeBuiler来创建迷宫。
@Test
public void CreateMazeTestBuilder(){
MazeGame game = new MazeGame();
StandarMazeBuilder smb = new StandarMazeBuilder();
game.CreateMaze(smb);
Maze maze = smb.getMaze();
}
我们本可以把 StandarMazeBuilder的所有操作都放进 Maze,让Maze自己创建自己。但是把Maze弄小一些,可以使我们更加容易理解和修改,StandarMazeBuilder 很容易从Maze中分离开来。但最重要的是,把这两者分离开来,能够使你更有机会有不同版本的MazeBuilder.每个MazeBuilder都能够创建不同的房间,门和墙壁。
一个更加独特的MazeBuilder是CountingMazeBuilder。这个builder根本不创建迷宫。它的作用就是计数组成这个迷宫的组件。
package com.hermeslch.pattern;
public class CoutingMazeBuilder implements MazeBuilder {
public CoutingMazeBuilder(){
room = 0;
door = 0;
}
@Override
public void buildMaze() {
// TODO Auto-generated method stub
}
@Override
public void buildRoom(int room) {
// TODO Auto-generated method stub
room ++;
}
@Override
public void buildDoor(int roomFrom, int roomTo) {
// TODO Auto-generated method stub
door++;
}
@Override
public Maze getMaze() {
// TODO Auto-generated method stub
return null;
}
public int getCountOfRoom(){
return room;
}
public int getCountOfDoor(){
return door;
}
private int room;
private int door;
}
这个类的构造函数初始化其内部的计数器,然后通过相应的增加计数器,重写MazeBuilder的操作函数。
下面是描述一个客户端代码怎么去使用CoutingMazeBuilder的。
MazeGame game = new MazeGame();
CoutingMazeBuilder builder = new CountingMazeBuilder();
game.CreateMaze(builder);
System.out.println("the Maze has " + builder.getCountofRoom() + "rooms and " + builder.getCountofDoor() + "doors" )
十 相关模式
抽象工厂模式和建筑者模式相同的一点是他们创建的对象都很复杂。最主要的区别在于建筑者模式的重点在于它关注复杂对象一步一步的创建。抽象工厂的关注重点在于整个家族产品的创建。建筑者模式在最后一步才返回产品对象,而抽象工厂模式关注的是立刻返回产品对象。
一个复合材料(Composite)经常是用建筑者模式来创建的。