`
mmdev
  • 浏览: 12885415 次
  • 性别: Icon_minigender_1
  • 来自: 大连
文章分类
社区版块
存档分类
最新评论

好书整理系列之-设计模式:可复用面向对象软件的基础 3

阅读更多


第3章创建型模式
创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合和表示它的那
些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委
托给另一个对象。
随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这
种情况发生时,重心从对一组固定行为的硬编码( h a r d - c o d i n g)转移为定义一个较小的基本
行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求
的不仅仅是实例化一个类。
在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类
的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关
于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么被创建, 谁创建它,
它是怎样被创建的,以及何时创建这些方面给予你很大的灵活性。它们允许你用结构和功能
差别很大的“产品”对象配置一个系统。配置可以是静态的(即在编译时指定),也可以是动
态的(在运行时)。
有时创建型模式是相互竞争的。例如,在有些情况下P r o t o t y p e(3 . 4)或Abstract Factory
(3 . 1)用起来都很好。而在另外一些情况下它们是互补的: B u i l d e r(3 . 2)可以使用其他模式
去实现某个构件的创建。P r o t o t y p e(3 . 4)可以在它的实现中使用S i n g l e t o n(3 . 5)。
因为创建型模式紧密相关,我们将所有5个模式一起研究以突出它们的相似点和相异点。
我们也将举一个通用的例子—为一个电脑游戏创建一个迷宫—来说明它们的实现。这个迷
宫和游戏将随着各种模式不同而略有区别。有时这个游戏将仅仅是找到一个迷宫的出口;在
这种情况下,游戏者可能仅能见到该迷宫的局部。有时迷宫包括一些要解决的问题和要战胜
的危险,并且这些游戏可能会提供已经被探索过的那部分迷宫地图。
我们将忽略许多迷宫中的细节以及一个迷宫游戏中有一个还是多个游戏者。我们仅关注
迷宫是怎样被创建的。我们将一个迷宫定义为一系列房间,一个房间知道它的邻居;可能的
邻居要么是另一个房间,要么是一堵墙、或者是到另一个房间的一扇门。
类R o o m、D o o r和Wa l l定义了我们所有的例子中使用到的构件。我们仅定义这些类中对创
建一个迷宫起重要作用的一些部分。我们将忽略游戏者、显示操作和在迷宫中四处移动操作,
以及其他一些重要的却与创建迷宫无关的功能。
下页图表示了这些类之间的关系。
每一个房间有四面,我们使用C + +中的枚举类型D i r e c t i o n来指定房间的东南西北:
enum Direction {North, South, East, West};
S m a l l t a l k的实现使用相应的符号来表示这些方向。
类M a p S i t e是所有迷宫组件的公共抽象类。为简化例子, M a p S i t e仅定义了一个操作E n t e r,
它的含义决定于你在进入什么。如果你进入一个房间,那么你的位置会发生改变。如果你试
图进入一扇门,那么这两件事中就有一件会发生:如果门是开着的,你进入另一个房间。如
果门是关着的,那么你就会碰壁。
E n t e r为更加复杂的游戏操作提供了一个简单基础。例如,如果你在一个房间中说“向东
走”,游戏只能确定直接在东边的是哪一个M a p S i t e并对它调用E n t e r。特定子类的E n t e r操作将
计算出你的位置是发生改变,还是你会碰壁。在一个真正的游戏中, E n t e r可以将移动的游戏
者对象作为一个参数。
R o o m是M a p S i t e的一个具体的子类,而M a p S i t e定义了迷宫中构件之间的主要关系。
R o o m有指向其他M a p S i t e对象的引用,并保存一个房间号,这个数字用来标识迷宫中的房间。
下面的类描述了一个房间的每一面所出现的墙壁或门。
第3章创建型模式5 5

我们不仅需要知道迷宫的各部分,还要定义一个用来表示房间集合的M a z e类。用
R o o m N o操作和给定的房间号, M a z e就可以找到一个特定的房间。
R o o m N o可以使用线形搜索、h a s h表、甚至一个简单数组进行一次查找。但我们在此处并
不考虑这些细节,而是将注意力集中于如何指定一个迷宫对象的构件上。
我们定义的另一个类是M a z e G a m e,由它来创建迷宫。一个简单直接的创建迷宫的方法是
使用一系列操作将构件增加到迷宫中,然后连接它们。例如,下面的成员函数将创建一个迷
宫,这个迷宫由两个房间和它们之间的一扇门组成:
考虑到这个函数所做的仅是创建一个有两个房间的迷宫,它是相当复杂的。显然有办法使它
变得更简单。例如, R o o m的构造器可以提前用墙壁来初始化房间的每一面。但这仅仅是将代
码移到了其他地方。这个成员函数真正的问题不在于它的大小而在于它不灵活。它对迷宫的
布局进行硬编码。改变布局意味着改变这个成员函数,或是重定义它—这意味着重新实现
整个过程—或是对它的部分进行改变—这容易产生错误并且不利于重用。
创建型模式显示如何使得这个设计更灵活,但未必会更小。特别是,它们将便于修改定
5 6 设计模式:可复用面向对象软件的基础

义一个迷宫构件的类。
假设你想在一个包含(所有的东西)施了魔法的迷宫的新游戏中重用一个已有的迷宫布
局。施了魔法的迷宫游戏有新的构件,像D o o r N e e d i n g S p e l l,它是一扇仅随着一个咒语才能被
锁上和打开的门;以及E n c h a n t e d R o o m,一个可以有不寻常东西的房间,比如魔法钥匙或是
咒语。你怎样才能较容易的改变C r e a t e M a z e以让它用这些新类型的对象创建迷宫呢?
这种情况下,改变的最大障碍是对被实例化的类进行硬编码。创建型模式提供了多种不
同方法从实例化它们的代码中除去对这些具体类的显式引用:
• 如果C r e a t e M a z e调用虚函数而不是构造器来创建它需要的房间、墙壁和门,那么你可以
创建一个M a z e G a m e的子类并重定义这些虚函数,从而改变被例化的类。这一方法是
Factory Method(3 . 3)模式的一个例子。
• 如果传递一个对象给C r e a t e M a z e作参数来创建房间、墙壁和门,那么你可以传递不同的
参数来改变房间、墙壁和门的类。这是Abstract Factory(3 . 1)模式的一个例子。
• 如果传递一个对象给C r e a t e M a z e,这个对象可以在它所建造的迷宫中使用增加房间、墙
壁和门的操作,来全面创建一个新的迷宫,那么你可以使用继承来改变迷宫的一些部分
或该迷宫被建造的方式。这是B u i l d e r(3 . 2)模式的一个例子。
• 如果C r e a t e M a z e由多种原型的房间、墙壁和门对象参数化,它拷贝并将这些对象增加到
迷宫中,那么你可以用不同的对象替换这些原型对象以改变迷宫的构成。这是P r o t o t y p e
(3 . 4)模式的一个例子。
剩下的创建型模式, S i n g l e t o n(3 . 5),可以保证每个游戏中仅有一个迷宫而且所有的游戏
对象都可以迅速访问它—不需要求助于全局变量或函数。S i n g l e t o n也使得迷宫易于扩展或替
换,且不需变动已有的代码。
3.1 ABSTRACT FACTORY(抽象工厂)—对象创建型模式
1. 意图
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
2. 别名
K i t
3. 动机
考虑一个支持多种视感( l o o k - a n d - f e e l)标准的用户界面工具包,例如M o t i f和
Presentation Manager。不同的视感风格为诸如滚动条、窗口和按钮等用户界面“窗口组件”
定义不同的外观和行为。为保证视感风格标准间的可移植性,一个应用不应该为一个特定的
视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后
很难改变视感风格。
为解决这一问题我们可以定义一个抽象的Wi d g e t F a c t o r y类,这个类声明了一个用来创建
每一类基本窗口组件的接口。每一类窗口组件都有一个抽象类,而具体子类则实现了窗口组
件的特定视感风格。对于每一个抽象窗口组件类, Wi d g e t F a c t o r y接口都有一个返回新窗口组
件对象的操作。客户调用这些操作以获得窗口组件实例,但客户并不知道他们正在使用的是
哪些具体类。这样客户就不依赖于一般的视感风格,如下页图所示。
第3章创建型模式5 7

每一种视感标准都对应于一个具体的Wi d g e t F a c t o r y子类。每一子类实现那些用于创建合
适视感风格的窗口组件的操作。例如, M o t i f Wi d g e t F a c t o r y的C r e a t e S c r o l l B a r操作实例化并返
回一个M o t i f滚动条,而相应的P M Wi d g e t F a c t o r y操作返回一个Presentation Manager的滚动条。
客户仅通过Wi d g e t F a c t o r y接口创建窗口组件,他们并不知道哪些类实现了特定视感风格的窗
口组件。换言之,客户仅与抽象类定义的接口交互,而不使用特定的具体类的接口。
Wi d g e t F a c t o r y也增强了具体窗口组件类之间依赖关系。一个M o t i f的滚动条应该与M o t i f按
钮、M o t i f正文编辑器一起使用,这一约束条件作为使用M o t i f Wi d g e t F a c t o r y的结果被自动加
上。
4. 适用性
在以下情况可以使用Abstract Factory模式
• 一个系统要独立于它的产品的创建、组合和表示时。
• 一个系统要由多个产品系列中的一个来配置时。
• 当你要强调一系列相关的产品对象的设计以便进行联合使用时。
• 当你提供一个产品类库,而只想显示它们的接口而不是实现时。
5. 结构
此模式的结构如下图所示。
6. 参与者
• A b s t r a c t F a c t o r y ( Wi d g e t F a c t o r y )
5 8 设计模式:可复用面向对象软件的基础

— 声明一个创建抽象产品对象的操作接口。
• C o n c r e t e F a c t o r y ( M o t i f Wi d g e t F a c t o r y,P M Wi d g e t F a c t o r y )
— 实现创建具体产品对象的操作。
• A b s t r a c t P r o d u c t ( Wi n d o w s,S c r o l l B a r )
— 为一类产品对象声明一个接口。
• C o n c r e t e P r o d u c t ( M o t i f Wi n d o w,M o t i f S c r o l l B a r )
— 定义一个将被相应的具体工厂创建的产品对象。
— 实现A b s t r a c t P r o d u c t接口。
• C l i e n t
— 仅使用由A b s t r a c t F a c t o r y和A b s t r a c t P r o d u c t类声明的接口。
7. 协作
• 通常在运行时刻创建一个C o n c r e t e F a c t r o y类的实例。这一具体的工厂创建具有特定实现
的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
• AbstractFactory将产品对象的创建延迟到它的C o n c r e t e F a c t o r y子类。
8. 效果
A b s t r a c t F a c t o r y模式有下面的一些优点和缺点:
1) 它分离了具体的类Abstract Factory模式帮助你控制一个应用创建的对象的类。因为
一个工厂封装创建产品对象的责任和过程,它将客户与类的实现分离。客户通过它们的抽象
接口操纵实例。产品的类名也在具体工厂的实现中被分离;它们不出现在客户代码中。
2) 它使得易于交换产品系列一个具体工厂类在一个应用中仅出现一次—即在它初始化
的时候。这使得改变一个应用的具体工厂变得很容易。它只需改变具体的工厂即可使用不同
的产品配置,这是因为一个抽象工厂创建了一个完整的产品系列,所以整个产品系列会立刻
改变。在我们的用户界面的例子中,我们仅需转换到相应的工厂对象并重新创建接口,就可
实现从M o t i f窗口组件转换为Presentation Manager窗口组件。
3) 它有利于产品的一致性当一个系列中的产品对象被设计成一起工作时,一个应用一
次只能使用同一个系列中的对象,这一点很重要。而A b s t r a c t F a c t o r y很容易实现这一点。
4) 难以支持新种类的产品难以扩展抽象工厂以生产新种类的产品。这是因为
A b s t r a c t F a c t o r y接口确定了可以被创建的产品集合。支持新种类的产品就需要扩展该工厂接口,
这将涉及A b s t r a c t F a c t o r y类及其所有子类的改变。我们会在实现一节讨论这个问题的一个解决
办法。
9. 实现
下面是实现Abstract Factor模式的一些有用技术:
1) 将工厂作为单件一个应用中一般每个产品系列只需一个C o n c r e t e F a c t o r y的实例。因此
工厂通常最好实现为一个S i n g l e t o n(3 . 5)。
2) 创建产品A b s t r a c t F a c t o r y仅声明一个创建产品的接口,真正创建产品是由
C o n c r e t e P r o d u c t子类实现的。最通常的一个办法是为每一个产品定义一个工厂方法(参见
Factory Method(3 . 3))。一个具体的工厂将为每个产品重定义该工厂方法以指定产品。虽然
这样的实现很简单,但它却要求每个产品系列都要有一个新的具体工厂子类,即使这些产品
系列的差别很小。
第3章创建型模式5 9

如果有多个可能的产品系列,具体工厂也可以使用P r o t o t y p e(3 . 4)模式来实现。具体工
厂使用产品系列中每一个产品的原型实例来初始化,且它通过复制它的原型来创建新的产品。
在基于原型的方法中,使得不是每个新的产品系列都需要一个新的具体工厂类。
此处是S m a l l t a l k中实现一个基于原型的工厂的方法。具体工厂在一个被称为p a r t C a t a l o g的
字典中存储将被复制的原型。方法m a k e:检索该原型并复制它:
make : partName
^ (partCatalog at : partName) copy
具体工厂有一个方法用来向该目录中增加部件。
addPart : partTemplate named : partName
partCatalog at : partName put : partTemplate
原型通过用一个符号标识它们,从而被增加到工厂中:
aFactory addPart : aPrototype named : #ACMEWidget
在将类作为第一类对象的语言中(例如S m a l l t a l k和O b j e c t i v e C),这个基于原型的方法可
能有所变化。你可以将这些语言中的一个类看成是一个退化的工厂,它仅创建一种产品。你
可以将类存储在一个具体工厂中,这个具体工厂在变量中创建多个具体的产品,这很像原型。
这些类代替具体工厂创建了新的实例。你可以通过使用产品的类而不是子类初始化一个具体
工厂的实例,来定义一个新的工厂。这一方法利用了语言的特点,而纯基于原型的方法是与
语言无关的。
像刚讨论过的S m a l l t a l k中的基于原型的工厂一样,基于类的版本将有一个唯一的实例变
量p a r t C a t a l o g,它是一个字典,它的主键是各部分的名字。p a r t C a t a l o g存储产品的类而不是存
储被复制的原型。方法m a k e:现在是这样:
make : partName
^ (partCatalog at : partName) new
3) 定义可扩展的工厂A b s t r a c t F a c t o r y通常为每一种它可以生产的产品定义一个操作。产
品的种类被编码在操作型构中。增加一种新的产品要求改变A b s t r a c t F a c t o r y的接口以及所有与
它相关的类。一个更灵活但不太安全的设计是给创建对象的操作增加一个参数。该参数指定
了将被创建的对象的种类。它可以是一个类标识符、一个整数、一个字符串,或其他任何可
以标识这种产品的东西。实际上使用这种方法, A b s t r a c t F a c t o r y只需要一个“ M a k e”操作和
一个指示要创建对象的种类的参数。这是前面已经讨论过的基于原型的和基于类的抽象工厂
的技术。
C + +这样的静态类型语言与相比,这一变化更容易用在类似于S m a l l t a l k这样的动态类型语
言中。仅当所有对象都有相同的抽象基类,或者当产品对象可以被请求它们的客户安全的强
制转换成正确类型时,你才能够在C + +中使用它。Factory Method(3.3)的实现部分说明了怎样
在C + +中实现这样的参数化操作。
该方法即使不需要类型强制转换,但仍有一个本质的问题:所有的产品将返回类型所给
定的相同的抽象接口返回给客户。客户将不能区分或对一个产品的类别进行安全的假定。如
果一个客户需要进行与特定子类相关的操作,而这些操作却不能通过抽象接口得到。虽然客
户可以实施一个向下类型转换( d o w n c a s t)(例如在C + +中用d y n a m i c _ c a s t),但这并不总是可
行或安全的,因为向下类型转换可能会失败。这是一个典型的高度灵活和可扩展接口的权衡
6 0 设计模式:可复用面向对象软件的基础

折衷。
10. 代码示例
我们将使用Abstract Factory模式创建我们在这章开始所讨论的迷宫。
类M a z e F a c t o r y可以创建迷宫的组件。它建造房间、墙壁和房间之间的门。它可以用于一
个从文件中读取迷宫说明图并建造相应迷宫的程序。或者它可以被用于一个随机建造迷宫的
程序。建造迷宫的程序将M a z e F a c t o r y作为一个参数,这样程序员就能指定要创建的房间、墙
壁和门等类。
回想一下建立一个由两个房间和它们之间的门组成的小迷宫的成员函数C r e a t e M a z e。
C r e a t e M a z e对类名进行硬编码,这使得很难用不同的组件创建迷宫。
这里是一个以M a z e F a c t o r y为参数的新版本的CreateMaze ,它修改了以上缺点:
我们创建M a z e F a c t o r y的子类E n c h a n t e d M a z e F a c t o r y,这是一个创建施了魔法的迷宫的工
厂。E n c h a n t e d M a z e F a c t o r y将重定义不同的成员函数并返回R o o m,Wa l l等不同的子类。
第3章创建型模式6 1

现在假设我们想生成一个迷宫游戏,在这个游戏里,每个房间中可以有一个。如果
这个爆炸,它将(至少)毁坏墙壁。我们可以生成一个R o o m的子类以明了是否有一个炸
弹在房间中以及该是否爆炸了。我们也将需要一个Wa l l的子类以明了对墙壁的损坏。我
们将称这些类为R o o m Wi t h A B o m b和B o m b e d Wa l l。
我们将定义的最后一个类是B o m b e d M a z e F a c t o r y,它是M a z e F a c t o r y的子类,保证了墙壁
是B o m b e d Wa l l类的而房间是R o o m Wi t h A B o m b的。B o m b e d M a z e F a c t o r y仅需重定义两个函数:
为建造一个包含的简单迷宫,我们仅用B o m b e d M a z e F a c t o r y调用C r e a t e M a z e。
MazeGame game;
BombedMazeFactory factory;
g a m e . C r e a t e M a z e ( f a c t o r y ) ;
C r e a t e M a z e也可以接收一个E n c h a n t e d M a z e F a c t o r y实例来建造施了魔法的迷宫。
注意M a z e F a c t o r y仅是工厂方法的一个集合。这是最通常的实现Abstract Factory模式的方
式。同时注意M a z e F a c t o r y 不是一个抽象类;因此它既作为A b s t r a c t F a c t o r y 也作为
C o n c r e t e F a c t o r y。这是Abstract Factory 模式的简单应用的另一个通常的实现。因为
M a z e F a c t o r y是一个完全由工厂方法组成的具体类,通过生成一个子类并重定义需要改变的操
作,它很容易生成一个新的M a z e F a c t o r y。
C r e a t e M a z e使用房间的S e t S i d e操作以指定它们的各面。如果它用一个B o m b e d M a z e F a c t o r y
创建房间,那么该迷宫将由有B o m b e d Wa l l 面的R o o m Wi t h A B o m b 对象组成。如果
R o o m Wi t h A B o m b必须访问一个B o m b e d Wa l l的与特定子类相关的成员,那么它将不得不对它的
墙壁引用以进行从Wa l l *到B o m b e d Wa l l *的转换。只要该参数确实是一个B o m b e d Wa l l,这个向
下类型转换就是安全的,而如果墙壁仅由一个B o m b e d M a z e F a c t o r y创建就可以保证这一点。
当然,像S m a l l t a l k这样的动态类型语言不需要向下类型转换,但如果它们在应该是Wa l l
的子类的地方遇到一个Wa l l类可能会产生运行时刻错误。使用Abstract Factory建造墙壁,通
过确定仅有特定类型的墙壁可以被创建,从而有助于防止这些运行时刻错误。
让我们考虑一个S m a l l t a l k版本的M a z e F a c t o r y,它仅有一个以要生成的对象种类为参数的
m a k e操作。此外,具体工厂存储它所创建的产品的类。
首先,我们用S m a l l t a l k写一个等价的C r e a t e M a z e:
6 2 设计模式:可复用面向对象软件的基础

正如我们在实现一节所讨论, M a z e F a c t o r y仅需一个实例变量p a r t C a t a l o g来提供一个字典,
这个字典的主键为迷宫组件的类。也回想一下我们是如何实现m a k e :方法的:
现在我们可以创建一个M a z e F a c t o r y并用它来实现C r e a t e M a z e。我们将用类M a z e G a m e的
一个方法C r e a t e M a z e F a c t o r y来创建该工厂。
通过将不同的类与它们的主键相关联,就可以创建一个B o m b e d M a z e F a c t o r y或E n c h a n t e d
M a z e F a c t o r y。例如,一个E n c h a n t e d M a z e F a c t o r y可以这样被创建:
11. 已知应用
I n t e r Vi e w使用“K i t”后缀[ L i n 9 2 ]来表示A b s t r a c t F a c t o r y类。它定义Wi d g e t K i t和D i a l o g K i t
抽象工厂来生成与特定视感风格相关的用户界面对象。I n t e r Vi e w还包括一个L a y o u t K i t,它根
据所需要的布局生成不同的组成( c o m p o s i t i o n)对象。例如,一个概念上是水平的布局根据
文档的定位(画像或是风景)可能需要不同的组成对象。
E T + + [ W G M 8 8 ]使用Abstract Factory模式以达到在不同窗口系统(例如, X Wi n d o w s和
S u n Vi e w)间的可移植性。Wi n d o w S y s t e m抽象基类定义一些接口,来创建表示窗口系统资源
的对象(例如M a k e Wi n d o w、M a k e F o n t、M a k e C o l o r)。具体的子类为某个特定的窗口系统实
现这些接口。运行时刻, E T + +创建一个具体Wi n d o w S y s t e m子类的实例,以创建具体的系统
资源对象。
12. 相关模式
A b s t r a c t F a c t o r y类通常用工厂方法( Factory Method (3 . 3))实现,但它们也可以用
P r o t o t y p e实现。
一个具体的工厂通常是一个单件( S i n g l e t o n(3 . 5))。
3.2 BUILDER(生成器)—对象创建型模式
1. 意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
第3章创建型模式6 3

2. 动机
一个RT F(Rich Text Format)文档交换格式的阅读器应能将RT F转换为多种正文格式。
该阅读器可以将RT F文档转换成普通A S C I I文本或转换成一个能以交互方式编辑的正文窗口组
件。但问题在于可能转换的数目是无限的。因此要能够很容易实现新的转换的增加,同时却
不改变RT F阅读器。
一个解决办法是用一个可以将RT F转换成另一种正文表示的Te x t C o n v e r t e r对象配置这个
RT F R e a d e r类。当RT F R e a d e r对RT F文档进行语法分析时,它使用Te x t C o n v e r t e r去做转换。无
论何时RT F R e a d e r识别了一个RT F标记(或是普通正文或是一个RT F控制字),它都发送一个
请求给Te x t C o n v e r t e r去转换这个标记。Te x t C o n v e r t e r对象负责进行数据转换以及用特定格式
表示该标记,如下图所示。
Te x t C o n v e r t的子类对不同转换和不同格式进行特殊处理。例如,一个A S C I I C o n v e r t e r只
负责转换普通文本,而忽略其他转换请求。另一方面,一个Te X C o n v e r t e r将会为实现对所有
请求的操作,以便生成一个获取正文中所有风格信息的T E X表示。一个Te x t Wi d g e t C o n v e r t e r
将生成一个复杂的用户界面对象以便用户浏览和编辑正文。
每种转换器类将创建和装配一个复杂对象的机制隐含在抽象接口的后面。转换器独立于
阅读器,阅读器负责对一个RT F文档进行语法分析。
B u i l d e r模式描述了所有这些关系。每一个转换器类在该模式中被称为生成器( b u i l d e r),
而阅读器则称为导向器( d i r e c t o r)。在上面的例子中, B u i l d e r模式将分析文本格式的算法
(即RT F文档的语法分析程序)与描述怎样创建和表示一个转换后格式的算法分离开来。这使
我们可以重用RT F R e a d e r的语法分析算法,根据RT F文档创建不同的正文表示—仅需使用不
同的Te x t C o n v e r t e r的子类配置该RT F R e a d e r即可。
3. 适用性
在以下情况使用B u i l d e r模式
• 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
• 当构造过程必须允许被构造的对象有不同的表示时。
4. 结构
此模式结构如下页上图所示。
6 4 设计模式:可复用面向对象软件的基础

5. 参与者
• B u i l d e r(Te x t C o n v e r t e r)
— 为创建一个P r o d u c t对象的各个部件指定抽象接口。
• C o n c r e t e B u i l d e r(A S C I I C o n v e r t e r、Te X C o n v e r t e r、Te x t Wi d g e t C o n v e r t e r)
— 实现B u i l d e r的接口以构造和装配该产品的各个部件。
— 定义并明确它所创建的表示。
— 提供一个检索产品的接口(例如, G e t A S C I I Te x t和G e t Te x t Wi d g e t)。
• Director(RT F R e a d e r)
— 构造一个使用B u i l d e r接口的对象。
• P r o d u c t(A S C I I Te x t、Te X Te x t、Te x t Wi d g e t)
— 表示被构造的复杂对象。C o n c r e t e B u i l d e r创建该产品的内部表示并定义它的装配过程。
— 包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
6. 协作
• 客户创建D i r e c t o r对象,并用它所想要的B u i l d e r对象进行配置。
• 一旦产品部件被生成,导向器就会通知生成器。
• 生成器处理导向器的请求,并将部件添加到该产品中。
• 客户从生成器中检索产品。
下面的交互图说明了B u i l d e r和D i r e c t o r是如何与一个客户协作的。
7. 效果
这里是B u i l d e r模式的主要效果:
第3章创建型模式6 5

1 ) 它使你可以改变一个产品的内部表示B u i l d e r对象提供给导向器一个构造产品的抽象
接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如
何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定
义一个新的生成器。
2) 它将构造代码和表示代码分开B u i l d e r模式通过封装一个复杂对象的创建和表示方式
提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息;这些类是不出现
在B u i l d e r接口中的。每个C o n c r e t e B u i l d e r包含了创建和装配一个特定产品的所有代码。这些
代码只需要写一次;然后不同的D i r e c t o r可以复用它以在相同部件集合的基础上构作不同的
P r o d u c t。在前面的RT F例子中,我们可以为RT F格式以外的格式定义一个阅读器,比如一个
S G M L R e a d e r,并使用相同的Te x t C o n v e r t e r生成S G M L文档的A S C I I Te x t、Te X Te x t和
Te x t Wi d g e t译本。
3 ) 它使你可对构造过程进行更精细的控制B u i l d e r模式与一下子就生成产品的创建型模
式不同,它是在导向者的控制下一步一步构造产品的。仅当该产品完成时导向者才从生成器
中取回它。因此B u i l d e r接口相比其他创建型模式能更好的反映产品的构造过程。这使你可以
更精细的控制构建过程,从而能更精细的控制所得产品的内部结构。
8. 实现
通常有一个抽象的B u i l d e r类为导向者可能要求创建的每一个构件定义一个操作。这些操
作缺省情况下什么都不做。一个C o n c r e t e B u i l d e r类对它有兴趣创建的构件重定义这些操作。
这里是其他一些要考虑的实现问题:
1) 装配和构造接口生成器逐步的构造它们的产品。因此B u i l d e r类接口必须足够普遍,
以便为各种类型的具体生成器构造产品。
一个关键的设计问题在于构造和装配过程的模型。构造请求的结果只是被添加到产品中,
通常这样的模型就已足够了。在RT F的例子中,生成器转换下一个标记并将它添加到它已经
转换了的正文中。
但有时你可能需要访问前面已经构造了的产品部件。我们在代码示例一节所给出的M a z e
例子中, M a z e B u i l d e r接口允许你在已经存在的房间之间增加一扇门。像语法分析树这样自底
向上构建的树型结构就是另一个例子。在这种情况下,生成器会将子结点返回给导向者,然
后导向者将它们回传给生成者去创建父结点。
2) 为什么产品没有抽象类通常情况下,由具体生成器生成的产品,它们的表示相差是
如此之大以至于给不同的产品以公共父类没有太大意思。在RT F例子中, A S C I I Te x t和
Te x t Wi d g e t对象不太可能有公共接口,它们也不需要这样的接口。因为客户通常用合适的具
体生成器来配置导向者,客户处于的位置使它知道B u i l d e r的哪一个具体子类被使用和能相应
的处理它的产品。
3 ) 在B u i l d e r中却省的方法为空C + +中,生成方法故意不声明为纯虚成员函数,而是把
它们定义为空方法,这使客户只重定义他们所感兴趣的操作。
9. 代码示例
我们将定义一个C r e a t e M a z e成员函数的变体,它以类M a z e B u i l d e r的一个生成器对象作为
参数。
M a z e B u i l d e r类定义下面的接口来创建迷宫:
6 6 设计模式:可复用面向对象软件的基础

该接口可以创建:1)迷宫。2)有一个特定房间号的房间。3)在有号码的房间之间的门。
G e t M a z e操作返回这个迷宫给客户。M a z e B u i l d e r的子类将重定义这些操作,返回它们所创建
的迷宫。
M a z e B u i l d e r的所有建造迷宫的操作缺省时什么也不做。不将它们定义为纯虚函数是为了
便于派生类只重定义它们所感兴趣的那些方法。
用M a z e B u i l d e r接口,我们可以改变C r e a t e M a z e成员函数,以生成器作为它的参数。
将这个C r e a t e M a z e版本与原来的相比,注意生成器是如何隐藏迷宫的内部表示的—即定
义房间、门和墙壁的那些类—以及这些部件是如何组装成最终的迷宫的。有人可能猜测到
有一些类是用来表示房间和门的,但没有迹象显示哪个类是用来表示墙壁的。这就使得改变
一个迷宫的表示方式要容易一些,因为所有M a z e B u i l d e r的客户都不需要被改变。
像其他创建型模式一样, B u i l d e r模式封装了对象是如何被创建的,在这个例子中是通过
M a z e B u i l d e r所定义的接口来封装的。这就意味着我们可以重用M a z e B u i l d e r来创建不同种类的
迷宫。C r e a t e C o m p l e x M a z e操作给出了一个例子:
注意M a z e B u i l d e r自己并不创建迷宫;它的主要目的仅仅是为创建迷宫定义一个接口。它
主要为方便起见定义一些空的实现。M a z e B u i l d e r的子类做实际工作。
子类S t a n d a r d M a z e B u i l d e r是一个创建简单迷宫的实现。它将它正在创建的迷宫放在变量
_ c u r r e n t M a z e中。
第3章创建型模式6 7

C o m m o n Wa l l是一个功能性操作,它决定两个房间之间的公共墙壁的方位。
S t a n d a r d M a z e B u i l d e r的构造器只初始化了_ c u r r e n t M a z e。
B u i l d M a z e实例化一个M a z e,它将被其他操作装配并最终返回给客户(通过G e t M a z e)。
B u i l d R o o m操作创建一个房间并建造它周围的墙壁:
为建造一扇两个房间之间的门, S t a n d a r d M a z e B u i l d e r查找迷宫中的这两个房间并找到它
们相邻的墙:
客户现在可以用C r e a t e M a z e和S t a n d a r d M a z e B u i l d e r来创建一个迷宫:
我们本可以将所有的S t a n d a r d M a z e B u i l d e r操作放在M a z e中并让每一个M a z e创建它自身。
但将M a z e变得小一些使得它能更容易被理解和修改,而且S t a n d a r d M a z e B u i l d e r易于从M a z e中
分离。更重要的是,将两者分离使得你可以有多种M a z e B u i l d e r,每一种使用不同的房间、墙
壁和门的类。
6 8 设计模式:可复用面向对象软件的基础

一个更特殊的M a z e B u i l d e r是C o u n t i n g M a z e B u i l d e r。这个生成器根本不创建迷宫;它仅仅
对已被创建的不同种类的构件进行计数。
构造器初始化该计数器,而重定义了的M a z e B u i l d e r操作只是相应的增加计数。
下面是一个客户可能怎样使用C o u n t i n g M a z e B u i l d e r:
10. 已知应用
RT F转换器应用来自E T + + [ W G M 8 8 ]。它的正文生成模块使用一个生成器处理以RT F格式
存储的正文。
生成器在S m a l l t a l k - 8 0 [ P a r 9 0 ]中是一个通用的模式:
• 编译子系统中的P a r s e r类是一个D i r e c t o r,它以一个P r o g r a m N o d e B u i l d e r对象作为参数。
每当P a r s e r对象识别出一个语法结构时,它就通知它的P r o g r a m N o d e B u i l d e r对象。当这
个语法分析器做完时,它向该生成器请求它生成的语法分析树并将语法分析树返回给客
户。
第3章创建型模式6 9

• C l a s s B u i l d e r是一个生成器, C l a s s使用它为自己创建子类。在这个例子中,一个C l a s s既
是D i r e c t o r也是P r o d u c t。
• B y t e C o d e S t r e a m 是一个生成器,它将一个被编译了的方法创建为字节数组。
B y t e C o d e S t r e a m不是B u i l d e r模式的标准使用,因为它生成的复杂对象被编码为一个字节
数组,而不是正常的S m a l l t a l k对象。但B y t e C o d e S t r e a m的接口是一个典型的生成器,而
且将很容易用一个将程序表示为复合对象的不同的类来替换B y t e C o d e S t r e a m。
自适应通讯环境( Adaptive Communications Environment )中的服务配置者( S e r v i c e
C o n f i g u r a t o r)框架使用生成器来构造运行时刻动态连接到服务器的网络服务构件[ S S 9 4 ]。这
些构件使用一个被L A L R(1)语法分析器进行语法分析的配置语言来描述。这个语法分析器
的语义动作对将信息加载给服务构件的生成器进行操作。在这个例子中,语法分析器就是
D i r e c t o r。
11. 相关模式
Abstract Factory (3 . 1)与B u i l d e r相似,因为它也可以创建复杂对象。主要的区别是
B u i l d e r模式着重于一步步构造一个复杂对象。而Abstract Factory着重于多个系列的产品对象
(简单的或是复杂的)。B u i l d e r在最后的一步返回产品,而对于Abstract Factory来说,产品是
立即返回的。
C o m p o s i t e(4 . 3)通常是用B u i l d e r生成的。
3.3 FACTORY METHOD(工厂方法)—对象创建型模式
1. 意图
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的
实例化延迟到其子类。
2. 别名
虚构造器( Virtual Constructor)
3. 动机
框架使用抽象类定义和维护对象之间的关系。这些对象的创建通常也由框架负责。
考虑这样一个应用框架,它可以向用户显示多个文档。在这个框架中,两个主要的抽象是
类A p p l i c a t i o n和D o c u m e n t。这两个类都是抽象的,客户必须通过它们的子类来做与具体应用相
关的实现。例如,为创建一个绘图应用,我们定义类D r a w i n g A p p l i c a t i o n和D r a w i n g D o c u m e n t。
A p p l i c a t i o n类负责管理D o c u m e n t并根据需要创建它们—例如,当用户从菜单中选择O p e n或
N e w的时候。
因为被实例化的特定D o c u m e n t子类是与特定应用相关的,所以A p p l i c a t i o n类不可能预测
到哪个D o c u m e n t子类将被实例化—A p p l i c a t i o n类仅知道一个新的文档何时应被创建,而不
知道哪一种D o c u m e n t将被创建。这就产生了一个尴尬的局面:框架必须实例化类,但是它只
知道不能被实例化的抽象类。
Factory Method模式提供了一个解决办案。它封装了哪一个D o c u m e n t子类将被创建的信
息并将这些信息从该框架中分离出来,如下页上图所示。
A p p l i c a t i o n的子类重定义A p p l i c a t i o n的抽象操作C r e a t e D o c u m e n t以返回适当的D o c u m e n t
子类对象。一旦一个A p p l i c a t i o n子类实例化以后,它就可以实例化与应用相关的文档,而无
7 0 设计模式:可复用面向对象软件的基础

需知道这些文档的类。我们称C r e a t e D o c u m e n t是一个工厂方法( f a c t o r y m e t h o d),因为它负
责“生产”一个对象。
4. 适用性
在下列情况下可以使用Factory Method模式:
• 当一个类不知道它所必须创建的对象的类的时候。
• 当一个类希望由它的子类来指定它所创建的对象的时候。
• 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类
是代理者这一信息局部化的时候。
5. 结构
6. 参与者
• P r o d u c t( D o c u m e n t )
— 定义工厂方法所创建的对象的接口。
• C o n c r e t e P r o d u c t(M y D o c u m e n t)
— 实现P r o d u c t接口。
• C r e a t o r(A p p l i c a t i o n)
— 声明工厂方法,该方法返回一个P r o d u c t类型的对象。C r e a t o r也可以定义一个工厂方
法的缺省实现,它返回一个缺省的C o n c r e t e P r o d u c t对象。
— 可以调用工厂方法以创建一个P r o d u c t对象。
• C o n c r e t e C r e a t o r(M y A p p l i c a t i o n)
— 重定义工厂方法以返回一个C o n c r e t e P r o d u c t实例。
7. 协作
• Creator依赖于它的子类来定义工厂方法,所以它返回一个适当的C o n c r e t e P r o d u c t实例。
8. 效果
第3章创建型模式7 1

工厂方法不再将与特定应用有关的类绑定到你的代码中。代码仅处理P r o d u c t接口;因此
它可以与用户定义的任何C o n c r e t e P r o d u c t类一起使用。
工厂方法的一个潜在缺点在于客户可能仅仅为了创建一个特定的C o n c r e t e P r o d u c t对象,
就不得不创建C r e a t o r的子类。当C r e a t o r子类不必需时,客户现在必然要处理类演化的其他方
面;但是当客户无论如何必须创建C r e a t o r的子类时,创建子类也是可行的。
下面是Factory Method模式的另外两种效果:
1 ) 为子类提供挂钩( h o o k) 用工厂方法在一个类的内部创建对象通常比直接创建对象
更灵活。Factory Method给子类一个挂钩以提供对象的扩展版本。
在D o c u m e n t的例子中, D o c u m e n t类可以定义一个称为C r e a t e F i l e D i a l o g的工厂方法,该方
法为打开一个已有的文档创建默认的文件对话框对象。D o c u m e n t的子类可以重定义这个工厂
方法以定义一个与特定应用相关的文件对话框。在这种情况下,工厂方法就不再抽象了而是
提供了一个合理的缺省实现。
2) 连接平行的类层次迄今为止,在我们所考虑的例子中,工厂方法并不往往只是被
C r e a t o r调用,客户可以找到一些有用的工厂方法,尤其在平行类层次的情况下。
当一个类将它的一些职责委托给一个独立的类的时候,就产生了平行类层次。考虑可以
被交互操纵的图形;也就是说,它们可以用鼠标进行伸展、移动,或者旋转。实现这样一些
交互并不总是那么容易,它通常需要存储和更新在给定时刻记录操纵状态的信息,这个状态
仅仅在操纵时需要。因此它不需要被保存在图形对象中。此外,当用户操纵图形时,不同的
图形有不同的行为。例如,将直线图形拉长可能会产生一个端点被移动的效果,而伸展正文
图形则可能会改变行距。
有了这些限制,最好使用一个独立的M a n i p u l a t o r对象实现交互并保存所需要的任何与特
定操纵相关的状态。不同的图形将使用不同的M a n i p u l a t o r子类来处理特定的交互。得到的
M a n i p u l a t o r类层次与F i g u r e类层次是平行(至少部分平行),如下图所示。
F i g u r e类提供了一个C r e a t e M a n i p u l a t o r工厂方法,它使得客户可以创建一个与F i g u r e相对
应的M a n i p u l a t o r。F i g u r e子类重定义该方法以返回一个合适的M a n i p u l a t o r子类实例。做为一
种选择, F i g u r e类可以实现C r e a t e M a n i p u l a t o r以返回一个默认的M a n i p u l a t o r实例,而F i g u r e子
类可以只是继承这个缺省实现。这样的F i g u r e类不需要相应的M a n i p u l a t o r子类—因此该层次
只是部分平行的。
注意工厂方法是怎样定义两个类层次之间的连接的。它将哪些类应一同工作工作的信息
局部化了。
7 2 设计模式:可复用面向对象软件的基础

9. 实现
当应用Factory Method模式时要考虑下面一些问题:
1 ) 主要有两种不同的情况Factory Method模式主要有两种不同的情况: 1)第一种情况
是, C r e a t o r类是一个抽象类并且不提供它所声明的工厂方法的实现。2)第二种情况是,
C r e a t o r是一个具体的类而且为工厂方法提供一个缺省的实现。也有可能有一个定义了缺省实
现的抽象类,但这不太常见。
第一种情况需要子类来定义实现,因为没有合理的缺省实现。它避免了不得不实例化不
可预见类的问题。在第二种情况中,具体的C r e a t o r主要因为灵活性才使用工厂方法。它所遵
循的准则是,“用一个独立的操作创建对象,这样子类才能重定义它们的创建方式。”这条准
则保证了子类的设计者能够在必要的时候改变父类所实例化的对象的类。
2 ) 参数化工厂方法该模式的另一种情况使得工厂方法可以创建多种产品。工厂方法采
用一个标识要被创建的对象种类的参数。工厂方法创建的所有对象将共享P r o d u c t接口。在
D o c u m e n t的例子中,A p p l i c a t i o n可能支持不同种类的D o c u m e n t。你给C r e a t e D o c u m e n t传递一
个外部参数来指定将要创建的文档的种类。
图形编辑框架Unidraw [VL90]使用这种方法来重构存储在磁盘上的对象。U n i d r a w定义了
一个C r e a t o r类,该类拥有一个以类标识符为参数的工厂方法C r e a t e。类标识符指定要被实例
化的类。当U n i d r a w将一个对象存盘时,它首先写类标识符,然后是它的实例变量。当它从磁
盘中重构该对象时,它首先读取的是类标识符。
一旦类标识符被读取后,这个框架就将该标识符作为参数,调用C r e a t e。C r e a t e到构造器
中查询相应的类并用它实例化对象。最后, C r e a t e调用对象的R e a d操作,读取磁盘上剩余的
信息并初始化该对象的实例变量。
一个参数化的工厂方法具有如下的一般形式,此处M y P r o d u c t和Yo u r P r o d u c t是P r o d u c t的
子类:
重定义一个参数化的工厂方法使你可以简单而有选择性的扩展或改变一个C r e a t o r生产的
产品。你可以为新产品引入新的标识符,或可以将已有的标识符与不同的产品相关联。
例如,子类M y C r e a t o r可以交换M y P r o d u c t 和Yo u r P r o d u c t并且支持一个新的子类
T h e i r P r o d u c t:
第3章创建型模式7 3

注意这个操作所做的最后一件事是调用父类的C r e a t e。这是因为M y C r e a t o r : : C r e a t e仅在对
Y O U R S、M I N E和T H E I R S的处理上和父类不同。它对其他类不感兴趣。因此M y C r e a t o r扩展
了所创建产品的种类,并且将除少数产品以外所有产品的创建职责延迟给了父类。
3) 特定语言的变化和问题不同的语言有助于产生其他一些有趣的变化和警告( c a v e a t)。
S m a l l t a l k程序通常使用一个方法返回被实例化的对象的类。C r e a t o r工厂方法可以使用这
个值去创建一个产品,并且C o n c r e t e C r e a t o r可以存储甚至计算这个值。这个结果是对实例化
的C o n c r e t e P r o d u c t类型的一个更迟的绑定。
S m a l l t a l k版本的D o c u m e n t的例子可以在A p p l i c a t i o n中定义一个d o c u m e n t C l a s s方法。该方
法为实例化文档返回合适的D o c u m e n t类,其在M y A p p l i c a t i o n中的实现返回M y D o c u m e n t类。
这样在类A p p l i c a t i o n中我们有
在类M y A p p l i c a t i o n中我们有
它把将被实例化的类M y D o c u m e n t返回给A p p l i c a t i o n。一个更灵活的类似于参数化工厂方
法的办法是将被创建的类存储为A p p l i c a t i o n的一个类变量。你用这种方法在改变产品时就无
需用到A p p l i c a t i o n的子类。
C + +中的工厂方法都是虚函数并且常常是纯虚函数。一定要注意在C r e a t o r的构造器中不
要调用工厂方法—在C o n c r e t e C r e a t o r中该工厂方法还不可用。
只要你使用按需创建产品的访问者操作,很小心地访问产品,你就可以避免这一点。构
造器只是将产品初始化为0,而不是创建一个具体产品。访问者返回该产品。但首先它要检查
确定该产品的存在,如果产品不存在,访问者就创建它。这种技术有时被称为l a z y
i n i t i a l i z a t i o n。下面的代码给出了一个典型的实现:
4 ) 使用模板以避免创建子类正如我们已经提及的,工厂方法另一个潜在的问题是它们
可能仅为了创建适当的P r o d u c t对象而迫使你创建C r e a t o r子类。在C + +中另一个解决方法是提
供C r e a t o r的一个模板子类,它使用P r o d u c t类作为模板参数:
7 4 设计模式:可复用面向对象软件的基础

使用这个模板,客户仅提供产品类—而不需要创建C r e a t o r的子类。
5 ) 命名约定使用命名约定是一个好习惯,它可以清楚地说明你正在使用工厂方法。例
如,M a c i n t o s h的应用框架MacApp [App89]总是声明那些定义为工厂方法的抽象操作为C l a s s *
DoMakeClass( ),此处C l a s s是P r o d u c t类。
10. 代码示例
函数C r e a t e M a z e(第3章)建造并返回一个迷宫。这个函数存在的一个问题是它对迷宫、
房间、门和墙壁的类进行了硬编码。我们将引入工厂方法以使子类可以选择这些构件。首先
我们将在M a z e G a m e中定义工厂方法以创建迷宫、房间、墙壁和门对象:
每一个工厂方法返回一个给定类型的迷宫构件。M a z e G a m e提供一些缺省的实现,它们返
回最简单的迷宫、房间、墙壁和门。
现在我们可以用这些工厂方法重写C r e a t e M a z e:
第3章创建型模式7 5

不同的游戏可以创建M a z e G a m e的子类以特别指明一些迷宫的部件。M a z e G a m e子类可以
重定义一些或所有的工厂方法以指定产品中的变化。例如,一个B o m b e d M a z e G a m e可以重定
义产品R o o m和Wa l l以返回爆炸后的变体:
一个E n c h a n t e d M a z e G a m e变体可以像这样定义:
11. 已知应用
工厂方法主要用于工具包和框架中。前面的文档例子是M a c A p p和E T + + [ W G M 8 8 ]中的一
个典型应用。操纵器的例子来自U n i d r a w。
Smalltalk-80 Model/Vi e w / C o n t r o l l e r框架中的类视图( Class Vi e w)有一个创建控制器的
方法d e f a u l t C o n t r o l l e r,它有点类似于一个工厂方法[ P a r 9 0 ]。但是Vi e w的子类通过定义
d e f a u l t C o n t r o l l e r C l a s s 来指定它们默认的控制器的类。d e f a u l t C o n t r o l l e r C l a s s返回
d e f a u l t C o n t r o l l e r所创建实例的类,因此它才是真正的工厂方法,即子类应该重定义它。
S m a l l t a l k - 8 0中一个更为深奥的例子是由B e h a v i o r(用来表示类的所有对象的超类)定义
的工厂方法p a r s e r C l a s s。这使得一个类可以对它的源代码使用一个定制的语法分析器。例如,
7 6 设计模式:可复用面向对象软件的基础

一个客户可以定义一个类S Q L P a r s e r来分析嵌入了S Q L语句的类的源代码。B e h a v i o r类实现了
p a r s e r C l a s s,返回一个标准的Smalltalk Parser类。一个包含嵌入S Q L语句的类重定义了该方法
(以类方法的形式)并返回S Q L P a r s e r类。
IONA Te c h n o l o g i e s的Orbix ORB系统[ I O N 9 4 ]在对象给一个远程对象引用发送请求时,使
用Factory Method生成一个适当类型的代理(参见P r o x y(4 . 7))。Factory Method使得易于替
换缺省代理。比如说,可以用一个使用客户端高速缓存的代理来替换。
12. 相关模式
Abstract Factory(3 . 1)经常用工厂方法来实现。Abstract Factory模式中动机一节的例子
也对Factory Method进行了说明。
工厂方法通常在Template Methods(5 . 1 0)中被调用。在上面的文档例子中,N e w D o c u m e n t
就是一个模板方法。
P r o t o t y p e s(3 . 4)不需要创建C r e a t o r的子类。但是,它们通常要求一个针对P r o d u c t类的
I n i t i a l i z e操作。C r e a t o r使用I n i t i a l i z e来初始化对象。而Factory Method不需要这样的操作。
3.4 PROTOTYPE(原型)—对象创建型模式
1. 意图
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
2. 动机
你可以通过定制一个通用的图形编辑器框架和增加一些表示音符、休止符和五线谱的新
对象来构造一个乐谱编辑器。这个编辑器框架可能有一个工具选择板用于将这些音乐对象加
到乐谱中。这个选择板可能还包括选择、移动和其他操纵音乐对象的工具。用户可以点击四
分音符工具并使用它将四分音符加到乐谱中。或者他们可以使用移动工具在五线谱上上下移
动一个音符,从而改变它的音调。
我们假定该框架为音符和五线谱这样的图形构件提供了一个抽象的G r a p h i c s类。此外,为
定义选择板中的那些工具,还提供一个抽象类To o l。该框架还为一些创建图形对象实例并将
它们加入到文档中的工具预定义了一个G r a p h i c To o l子类。
但G r a p h i c To o l给框架设计者带来一个问题。音符和五线谱的类特定于我们的应用,而
G r a p h i c To o l类却属于框架。G r a p h i c To o l不知道如何创建我们的音乐类的实例,并将它们添加
到乐谱中。我们可以为每一种音乐对象创建一个G r a p h i c To o l的子类,但这样会产生大量的子
类,这些子类仅仅在它们所初始化的音乐对象的类别上有所不同。我们知道对象复合是比创
建子类更灵活的一种选择。问题是,该框架怎么样用它来参数化G r a p h i c To o l的实例,而这些
实例是由G r a p h i c类所支持创建的。
解决办法是让G r a p h i c To o l通过拷贝或者“克隆”一个G r a p h i c子类的实例来创建新的
G r a p h i c,我们称这个实例为一个原型。G r a p h i c To o l将它应该克隆和添加到文档中的原型作为
参数。如果所有G r a p h i c子类都支持一个C l o n e操作,那么G r a p h i c To o l可以克隆所有种类的
G r a p h i c,如下页上图所示。
因此在我们的音乐编辑器中,用于创建个音乐对象的每一种工具都是一个用不同原型进
行初始化的G r a p h i c To o l实例。通过克隆一个音乐对象的原型并将这个克隆添加到乐谱中,每
个G r a p h i c To o l实例都会产生一个音乐对象。
第3章创建型模式7 7

我们甚至可以进一步使用P r o t o t y p e模式来减少类的数目。我们使用不同的类来表示全音
符和半音符,但可能不需要这么做。它们可以是使用不同位图和时延初始化的相同的类的实
例。一个创建全音符的工具就是这样的G r a p h i c To o l,它的原型是一个被初始化成全音符的
M u s i c a l N o t e。这可以极大的减少系统中类的数目,同时也更易于在音乐编辑器中增加新的音
符。
3. 适用性
当一个系统应该独立于它的产品创建、构成和表示时,要使用P r o t o t y p e模式;以及
• 当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者
• 为了避免创建一个与产品类层次平行的工厂类层次时;或者
• 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们
可能比每次用合适的状态手工实例化该类更方便一些。
4. 结构
5. 参与者
• P r o t o t y p e(G r a p h i c)
— 声明一个克隆自身的接口。
• C o n c r e t e P r o t o t y p e(S t a ff、W h o l e N o t e、H a l f N o t e)
— 实现一个克隆自身的操作。
• C l i e n t(G r a p h i c To o l)
— 让一个原型克隆自身从而创建一个新的对象。
7 8 设计模式:可复用面向对象软件的基础

6. 协作
• 客户请求一个原型克隆自身。
7. 效果
P r o t o t y p e有许多和Abstract Factory(3 . 1)和B u i l d e r(3 . 2)一样的效果:它对客户隐藏了
具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无需改变即可使
用与特定应用相关的类。
下面列出P r o t o t y p e模式的另外一些优点。
1 ) 运行时刻增加和删除产品P r o t o t y p e允许只通过客户注册原型实例就可以将一个新的
具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除
原型。
2 ) 改变值以指定新对象高度动态的系统允许你通过对象复合定义新的行为—例如,通
过为一个对象变量指定值—并且不定义新的类。你通过实例化已有类并且将这些实例注册
为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现
出新的行为。
这种设计使得用户无需编程即可定义新“类”。实际上,克隆一个原型类似于实例化一个
类。P r o t o t y p e模式可以极大的减少系统所需要的类的数目。在我们的音乐编辑器中,一个
G r a p h i c To o l类可以创建无数种音乐对象。
3) 改变结构以指定新对象许多应用由部件和子部件来创建对象。例如电路设计编辑器
就是由子电路来构造电路的。为方便起见,这样的应用通常允许你实例化复杂的、用户定
义的结构,比方说,一次又一次的重复使用一个特定的子电路。
P r o t o t y p e模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元
素选择板中。只要复合电路对象将C l o n e实现为一个深拷贝( deep copy),具有不同结构的电
路就可以是原型了。
4 ) 减少子类的构造Factory Method(3 . 3)经常产生一个与产品类层次平行的C r e a t o r类
层次。P r o t o t y p e模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因
此你根本不需要C r e a t o r类层次。这一优点主要适用于像C + +这样不将类作为一级类对象的语
言。像S m a l l t a l k和Objective C这样的语言从中获益较少,因为你总是可以用一个类对象作为
生成者。在这些语言中,类对象已经起到原型一样的作用了。
5) 用类动态配置应用一些运行时刻环境允许你动态将类装载到应用中。在像C + +这样的
语言中,P r o t o t y p e模式是利用这种功能的关键。
一个希望创建动态载入类的实例的应用不能静态引用类的构造器。而应该由运行环境在
载入时自动创建每个类的实例,并用原型管理器来注册这个实例(参见实现一节)。这样应用
就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。E T + +应用框
架[ W G M 8 8 ]有一个运行系统就是使用这一方案的。
P r o t o t y p e的主要缺陷是每一个P r o t o t y p e的子类都必须实现C l o n e操作,这可能很困难。例
如,当所考虑的类已经存在时就难以新增C l o n e操作。当内部包括一些不支持拷贝或有循环引
用的对象时,实现克隆可能也会很困难的。
8. 实现
第3章创建型模式7 9

这样的应用反映了C o m p o s i t e(4 . 3)和D e c o r a t o r(4 . 4)模式。
因为在像C + +这样的静态语言中,类不是对象,并且运行时刻只能得到很少或者得不到任
何类型信息,所以P r o t o t y p e特别有用。而在S m a l l t a l k或Objective C这样的语言中P r o t o t y p e就
不是那么重要了,因为这些语言提供了一个等价于原型的东西(即类对象)来创建每个类的
实例。P r o t o t y p e模式在像S e l f [ U S 8 7 ]这样基于原型的语言中是固有的,所有对象的创建都是通
过克隆一个原型实现的。
当实现原型时,要考虑下面一些问题:
1 ) 使用一个原型管理器当一个系统中原型数目不固定时(也就是说,它们可以动态创
建和销毁),要保持一个可用原型的注册表。客户不会自己来管理原型,但会在注册表中存储
和检索原型。客户在克隆一个原型前会向注册表请求该原型。我们称这个注册表为原型管理
器(prototype manager)。
原型管理器是一个关联存储器( associative store),它返回一个与给定关键字相匹配的原
型。它有一些操作可以用来通过关键字注册原型和解除注册。客户可以在运行时更改甚或浏
览这个注册表。这使得客户无需编写代码就可以扩展并得到系统清单。
2 ) 实现克隆操作P r o t o t y p e模式最困难的部分在于正确实现C l o n e操作。当对象结构包含
循环引用时,这尤为棘手。
大多数语言都对克隆对象提供了一些支持。例如, S m a l l t a l k提供了一个c o p y的实现,它
被所有O b j e c t的子类所继承。C + +提供了一个拷贝构造器。但这些设施并不能解决“浅拷贝和
深拷贝”问题[ G R 8 3 ]。也就是说,克隆一个对象是依次克隆它的实例变量呢,或者还是由克
隆对象和原对象共享这些变量?
浅拷贝简单并且通常也足够了,它是S m a l l t a l k所缺省提供的。C + +中的缺省拷贝构造器实
现按成员拷贝,这意味着在拷贝的和原来的对象之间是共享指针的。但克隆一个结构复杂的
原型通常需要深拷贝,因为复制对象和原对象必须相互独立。因此你必须保证克隆对象的构
件也是对原型的构件的克隆。克隆迫使你决定如果所有东西都被共享了该怎么办。
如果系统中的对象提供了S a v e和L o a d操作,那么你只需通过保存对象和立刻载入对象,
就可以为C l o n e操作提供一个缺省实现。S a v e操作将该对象保存在内存缓冲区中,而L o a d则通
过从该缓冲区中重构这个对象来创建一个复本。
3) 初始化克隆对象当一些客户对克隆对象已经相当满意时,另一些客户将会希望使用
他们所选择的一些值来初始化该对象的一些或是所有的内部状态。一般来说不可能在C l o n e操
作中传递这些值,因为这些值的数目由于原型的类的不同而会有所不同。一些原型可能需要
多个初始化参数,另一些可能什么也不要。在C l o n e操作中传递参数会破坏克隆接口的统一
性。
可能会这样,原型的类已经为(重)设定一些关键的状态值定义好了操作。如果这样的
话,客户在克隆后马上就可以使用这些操作。否则,你就可能不得不引入一个I n i t i a l i z e操作
(参见代码示例一节),该操作使用初始化参数并据此设定克隆对象的内部状态。注意深拷贝
C l o n e操作—一些复制在你重新初始化它们之前可能必须要被删除掉(删除可以显式地做也
可以在I n i t i a l i z e内部做)。
9. 代码示例
我们将定义M a z e F a c t o r y(3 . 1)的子类M a z e P r o t o t y p e F a c t o r y。该子类将使用它要创建的
对象的原型来初始化,这样我们就不需要仅仅为了改变它所创建的墙壁或房间的类而生成子
8 0 设计模式:可复用面向对象软件的基础

类了。
M a z e P r o t o t y p e F a c t o r y用一个以原型

分享到:
评论

相关推荐

    设计模式:可复用面向对象软件的基础--详细书签版

     本书结合设计实例从面向对象的设计中精选出23个设计模式,总结了面向对象设计中最有价值的经验,并且用简洁可复用的形式表达出来。本书分类描述了一组设计良好、表达清楚的软件设计模式,这些模式在实用环境下特别...

    设计模式可复用面向对象软件的基础(C++)——强烈推荐

    一本专门介绍设计模式的好书,基于C++的案例,该书深入浅出地介绍了设计模式的概念及应用。

    设计模式可复用面向对象软件的基础

    本书并不是一本介绍面向对象技术或设计的书,目前已有不少好书介绍面向对象技术或设计。本书假设你至少已经比较熟悉一种面向对象编程语言,并且有一定的面向对象设计经验。当我们提及“类型”和“多态”,或“接口”...

    设计模式:可复用面向对象技术

    four of gang 的好书! 设计模式的先祖!

    (经典)设计模式——可复用面向对象软件的基础(pdf)

    本书并不是一本介绍面向对象技术或设计的书,目前已有不少好书介绍面向对象技术或设计。本书假设你至少已经比较熟悉一种面向对象编程语言,并且有一定的面向对象设计经验。当我们提及“类型”和“多态”,或“接口”...

    设计模式.可复用面向对象软件的基础.中文版

    本书结合设计实作例从面向对象的设计中精选出23个设计模式,总结了面向对象设计中最有价值的经验,并且用简洁可复用的形式表达出来。本书分类描述了一组设计良好、表达清楚的软件设计模式,这些模式在实用环境下特别...

    设计模式可复用面向对象软件的基础(中文版)

    本书并不是一本介绍面向对象技术或设计的书,目前已有不少好书介绍面向对象技术或设计。本书假设你至少已经比较熟悉一种面向对象编程语言,并且有一定的面向对象设计经验。当我们提及“类型”和“多态”,或“接口”...

    设计模式-可复用面向对象软件的基础(高清)

    最经典的设计模式书籍,介绍23种设计模式,值得一生收藏的好书

    设计模式,可复用对象基础

    本书并不是一本介绍面向对象技术或设计的书,目前已有不少好书介绍面向对象技术或设计。本书假设你至少已经比较熟悉一种面向对象编程语言,并且有一定的面向对象设计经验。当我们提及“类型”和“多态”,或“接口”...

    敏捷软件开发-原则-模式与实践

    敏捷软件开发-原则-模式与实践 不可不读的好书

    java设计模式(设计实例从面向对象的设计中精选出23个设计模式)

    另外,这也不是一篇高级专题技术论文,而是一本关于设计模式的书,它描述了在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。设计模式捕获了随时间进化与发展的问题的求解方法,因此它们并不是人们从...

    设计模式 GOF 23

    本书设计实例从面向对象的设计中精选出23个设计模式,总结了面向对象设计中最有价值的经验,并且用简洁可复用的形式表达出来。本书分类描述了一组设计良好,表达清楚的软件设计模式,这些模式在实用环境下有特别有用...

    敏捷软件开发:原则、模式与实践

    软件开发的良好书,着重强调了敏捷开发软件的理念。

    深度探索 c++对象模型 中文版

    C++的面向对象的特性。这根本不能叫C++程序。(我想有时间重写一下以前代码也会有很多收获,温故而知新吗)C和C++在编程思想上 是相互矛盾的。这也就是说如果你想学C++,完全可以不学C,只需要一本好书和一个不太笨...

Global site tag (gtag.js) - Google Analytics