现在的位置: 首页 > 综合 > 正文

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

2013年01月04日 ⁄ 综合 ⁄ 共 24024字 ⁄ 字号 评论关闭

第4章结构型模式
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来
组合接口或实现。一个简单的例子是采用多重继承方法将两个以上的类组合成一个类,结果
这个类包含了所有父类的性质。这一模式尤其有助于多个独立开发的类库协同工作。另外一
个例子是类形式的A d a p t e r ( 4 . 1 )模式。一般来说,适配器使得一个接口( a d a p t e e的接口)与其他
接口兼容,从而给出了多个不同接口的统一抽象。为此,类适配器对一个a d a p t e e类进行私有
继承。这样,适配器就可以用a d a p t e e的接口表示它的接口。
结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从
而实现新功能的一些方法。因为可以在运行时刻改变对象组合关系,所以对象组合方式具有
更大的灵活性,而这种机制用静态类组合是不可能实现的。
Composite (4.3) 模式是结构型对象模式的一个实例。它描述了如何构造一个类层次式结
构,这一结构由两种类型的对象(基元对象和组合对象)所对应的类构成. 其中的组合对象使
得你可以组合基元对象以及其他的组合对象,从而形成任意复杂的结构。在Proxy (4.7) 模式
中,p r o x y对象作为其他对象的一个方便的替代或占位符。它的使用可以有多种形式。例如它
可以在局部空间中代表一个远程地址空间中的对象,也可以表示一个要求被加载的较大的对
象,还可以用来保护对敏感对象的访问。P r o x y模式还提供了对对象的一些特有性质的一定程
度上的间接访问,从而它可以限制、增强或修改这些性质。
F l y w e i g h t ( 4 . 6 )模式为了共享对象定义了一个结构。至少有两个原因要求对象共享:效率
和一致性。F l y w e i g h t的对象共享机制主要强调对象的空间效率。使用很多对象的应用必需考
虑每一个对象的开销。使用对象共享而不是进行对象复制,可以节省大量的空间资源。但是
仅当这些对象没有定义与上下文相关的状态时,它们才可以被共享。F l y w e i g h t的对象没有这
样的状态。任何执行任务时需要的其他一些信息仅当需要时才传递过去。由于不存在与上下
文相关的状态,因此F l y w e i g h t对象可以被自由地共享。
如果说F l y w e i g h t模式说明了如何生成很多较小的对象,那么F a c a d e ( 4 . 5 )模式则描述了如
何用单个对象表示整个子系统。模式中的f a c a d e用来表示一组对象, f a c a d e的职责是将消息转
发给它所表示的对象。B r i d g e ( 4 . 2 )模式将对象的抽象和其实现分离,从而可以独立地改变它
们。
D e c o r a t o r ( 4 . 4 )模式描述了如何动态地为对象添加职责。D e c o r a t o r模式是一种结构型模式。
这一模式采用递归方式组合对象,从而允许你添加任意多的对象职责。例如,一个包含用户
界面组件的D e c o r a t o r对象可以将边框或阴影这样的装饰添加到该组件中,或者它可以将窗口
滚动和缩放这样的功能添加的组件中。我们可以将一个D e c o r a t o r对象嵌套在另外一个对象中
就可以很简单地增加两个装饰,添加其他的装饰也是如此。因此,每个D e c o r a t o r对象必须与
其组件的接口兼容并且保证将消息传递给它。D e c o r a t o r模式在转发一条信息之前或之后都可
以完成它的工作(比如绘制组件的边框)。
许多结构型模式在某种程度上具有相关性,我们将在本章末讨论这些关系。
4.1 ADAPTER(适配器)—类对象结构型模式
1. 意图
将一个类的接口转换成客户希望的另外一个接口。A d a p t e r模式使得原本由于接口不兼容
而不能一起工作的那些类可以一起工作。
2. 别名
包装器Wr a p p e r。
3. 动机
有时,为复用而设计的工具箱类不能够被复用的原因仅仅是因为它的接口与专业应用领
域所需要的接口不匹配。
例如,有一个绘图编辑器,这个编辑器允许用户绘制和排列基本图元(线、多边型和正
文等)生成图片和图表。这个绘图编辑器的关键抽象是图形对象。图形对象有一个可编辑的
形状,并可以绘制自身。图形对象的接口由一个称为S h a p e的抽象类定义。绘图编辑器为每一
种图形对象定义了一个S h a p e的子类: L i n e S h a p e类对应于直线, P o l y g o n S h a p e类对应于多边
型,等等。
像L i n e S h a p e和P o l y g o n S h a p e这样的基本几何图形的类比较容易实现,这是由于它们的绘
图和编辑功能本来就很有限。但是对于可以显示和编辑正文的Te x t S h a p e子类来说,实现相当
困难,因为即使是基本的正文编辑也要涉及到复杂的屏幕刷新和缓冲区管理。同时,成品的
用户界面工具箱可能已经提供了一个复杂的Te x t Vi e w类用于显示和编辑正文。理想的情况是
我们可以复用这个Te x t Vi e w类以实现Te x t S h a p e类,但是工具箱的设计者当时并没有考虑
S h a p e的存在,因此Te x t Vi e w和S h a p e对象不能互换。
一个应用可能会有一些类具有不同的接口并且这些接口互不兼容,在这样的应用中象
Te x t Vi e w这样已经存在并且不相关的类如何协同工作呢?我们可以改变Te x t Vi e w类使它兼容
S h a p e类的接口,但前提是必须有这个工具箱的源代码。然而即使我们得到了这些源代码,修
改Te x t Vi e w也是没有什么意义的;因为不应该仅仅为了实现一个应用,工具箱就不得不采用
一些与特定领域相关的接口。
我们可以不用上面的方法,而定义一个Te x t S h a p e类,由它来适配Te x t Vi e w的接口和S h a p e
的接口。我们可以用两种方法做这件事: 1) 继承S h a p e类的接口和Te x t Vi e w的实现,或2) 将一
个Te x t Vi e w实例作为Te x t S h a p e的组成部分,并且使用Te x t Vi e w的接口实现Te x t S h a p e。这两种
方法恰恰对应于A d a p t e r模式的类和对象版本。我们将Te x t S h a p e称之为适配器A d a p t e r。
9 2 设计模式:可复用面向对象软件的基础

上面的类图说明了对象适配器实例。它说明了在S h a p e类中声明的B o u n d i n g B o x请求如何
被转换成在Te x t Vi e w类中定义的G e t E x t e n t请求。由于Te x t S h a p e将Te x t Vi e w的接口与S h a p e的
接口进行了匹配,因此绘图编辑器就可以复用原先并不兼容的Te x t Vi e w类。
A d a p t e r时常还要负责提供那些被匹配的类所没有提供的功能,上面的类图中说明了适配
器如何实现这些职责。由于绘图编辑器允许用户交互的将每一个S h a p e对象“拖动”到一个新
的位置,而Te x t Vi e w设计中没有这种功能。我们可以实现Te x t S h a p 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子类的一个实例。
M a n i p u l a t o r是一个抽象类,它所描述的对象知道如何驱动S h a p e类响应相应的用户输入,
例如将图形拖动到一个新的位置。对应于不同形状的图形, M a n i p u l a t o r有不同的子类;例如
子类Te x t M a n i p u l a t o r对应于Te x t S h a p e。Te x t S h a p e通过返回一个Te x t M a n i p u l a t o r实例,增加了
Te x t Vi e w中缺少而S h a p e需要的功能。
4. 适用性
以下情况使用A d a p t e r模式
• 你想使用一个已经存在的类,而它的接口不符合你的需求。
• 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口
可能不一定兼容的类)协同工作。
• (仅适用于对象A d a p t e r)你想使用一些已经存在的子类,但是不可能对每一个都进行
子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
5. 结构
类适配器使用多重继承对一个接口与另一个接口进行匹配,如下图所示。
对象匹配器依赖于对象组合,如下图所示。
6. 参与者
• Ta r g e t ( S h a p e )
— 定义C l i e n t使用的与特定领域相关的接口。
• C l i e n t ( D r a w i n g E d i t o r )
第4章结构型模式9 3

— 与符合Ta rg e t接口的对象协同。
• A d a p t e e ( Te x t Vi e w )
— 定义一个已经存在的接口,这个接口需要适配。
• A d a p t e r ( Te x t S h a p e )
— 对A d a p t e e的接口与Ta rg e t接口进行适配
7. 协作
• Client在A d a p t e r实例上调用一些操作。接着适配器调用A d a p t e e的操作实现这个请求。
8. 效果
类适配器和对象适配器有不同的权衡。类适配器
• 用一个具体的A d a p t e r类对A d a p t e e和Ta rg e t进行匹配。结果是当我们想要匹配一个类以
及所有它的子类时,类A d a p t e r将不能胜任工作。
• 使得A d a p t e r可以重定义A d a p t e e的部分行为,因为A d a p t e r是A d a p t e e的一个子类。
• 仅仅引入了一个对象,并不需要额外的指针以间接得到a d a p t e e。
对象适配器则
• 允许一个A d a p t e r与多个A d a p t e e—即A d a p t e e本身以及它的所有子类(如果有子类的话)
—同时工作。A d a p t e r也可以一次给所有的A d a p t e e添加功能。
• 使得重定义A d a p t e e的行为比较困难。这就需要生成A d a p t e e的子类并且使得A d a p t e r引用
这个子类而不是引用A d a p t e e本身。
使用A d a p t e r模式时需要考虑的其他一些因素有:
1) Adapter的匹配程度对A d a p t e e的接口与Ta rg e t的接口进行匹配的工作量各个A d a p t e r可
能不一样。工作范围可能是,从简单的接口转换(例如改变操作名)到支持完全不同的操作集
合。A d a p t e r的工作量取决于Ta rg e t接口与A d a p t e e接口的相似程度。
2) 可插入的Adapter 当其他的类使用一个类时,如果所需的假定条件越少,这个类就更
具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的
接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系
统对这个类的接口可能会有所不同。O b j e c t - Wo r k / S m a l l t a l k [ P a r 9 0 ]使用pluggable adapter一词
描述那些具有内部接口适配的类。
考虑Tr e e D i s p l a y窗口组件,它可以图形化显示树状结构。如果这是一个具有特殊用途的
窗口组件,仅在一个应用中使用,我们可能要求它所显示的对象有一个特殊的接口,即它们
都是抽象类Tr e e的子类。如果我们希望使Tr e e D i s p l a y有具有良好的复用性的话(比如说,我
们希望将它作为可用窗口组件工具箱的一部分),那么这种要求将是不合理的。应用程序将自
己定义树结构类,而不应一定要使用我们的抽象类Tr e e。不同的树结构会有不同的接口。
例如,在一个目录层次结构中,可以通过G e t S u b d i r e c t o r i e s操作进行访问子目录,然而在
一个继承式层次结构中,相应的操作可能被称为G e t S u b c l a s s e s。尽管这两种层次结构使用的
接口不同,一个可复用的Tr e e D i s p l a y窗口组件必须能显示所有这两种结构。也就是说,
Tr e e D i s p l a y应具有接口适配的功能。
我们将在实现一节讨论在类中构建接口适配的多种方法。
3) 使用双向适配器提供透明操作使用适配器的一个潜在问题是,它们不对所有的客户
都透明。被适配的对象不再兼容A d a p t e e的接口,因此并不是所有A d a p t e e对象可以被使用的
9 4 设计模式:可复用面向对象软件的基础

地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方
式查看同一个对象时,双向适配器尤其有用。
考虑一个双向适配器,它将图形编辑框架Unidraw [VL90] 与约束求解工具箱Q O C A
[ H H M V 9 2 ]集成起来。这两个系统都有一些类,这些类显式地表示变量: U n i d r a w含有类
S t a t e Va r i a b l e,Q O C A中含有类C o n s t r a i n t Va r i a b l e,如下图所示。为了使U n i d r a w与Q O C A协同
工作,必须首先使类C o n s t r a i n t Va r i a b l e与类S t a t e Va r i a b l e相匹配;而为了将Q O C A的求解结果
传递给U n i d r a w,必须使S t a t e Va r i a b l e与C o n s t r a i n t Va r i a b l e相匹配。
这一方案中包含了一个双向适配器C o n s t r a i n t S t a t e Va r i a b l e,它是类C o n s t r a i n t Va r i a b l e与类
S t a t e Va r i a b l e共同的子类, C o n s t r a i n t S t a t e Va r i a b l e使得两个接口互相匹配。在该例中多重继承
是一个可行的解决方案,因为被适配类的接口差异较大。双向适配器与这两个被匹配的类都
兼容,在这两个系统中它都可以工作。
9. 实现
尽管A d a p t e r模式的实现方式通常简单直接,但是仍需要注意以下一些问题:
1) 使用C + +实现适配器类在使用C + +实现适配器类时, A d a p t e r类应该采用公共方式继
承Ta rg e t类,并且用私有方式继承A d a p t e e类。因此, A d a p t e r类应该是Ta rg e t的子类型,但不
是A d a p t e e的子类型。
2) 可插入的适配器有许多方法可以实现可插入的适配器。例如,前面描述的Tr e e D i s p l a y
窗口组件可以自动的布置和显示层次式结构,对于它有三种实现方法:
首先(这也是所有这三种实现都要做的)是为Adaptee 找到一个“窄”接口,即可用于适
配的最小操作集。因为包含较少操作的窄接口相对包含较多操作的宽接口比较容易进行匹配。
对于Tr e e D i s p l a y而言,被匹配的对象可以是任何一个层次式结构。因此最小接口集合仅包含
两个操作:一个操作定义如何在层次结构中表示一个节点,另一个操作返回该节点的子节点。
对这个窄接口,有以下三个实现途径:
a) 使用抽象操作在Tr e e D i s p l a y类中定义窄A d a p t e e接口相应的抽象操作。这样就由子类
来实现这些抽象操作并匹配具体的树结构的对象。例如, D i r e c t o r y Tr e e D i s p l a y子类将通过访
问目录结构实现这些操作,如下图所示。
第4章结构型模式9 5

(到QOCA类层次结构) (到Unidraw 类层次结构)
D i r e c t o r y Tr e e D i s p l a y对这个窄接口加以特化,使得它的D i r e c t o r y B r o w s e r客户可以用它来
显示目录结构。
b) 使用代理对象在这种方法中, Tr e e D i s p l a y将访问树结构的请求转发到代理对象。
Tr e e D i s p l a y的客户进行一些选择,并将这些选择提供给代理对象,这样客户就可以对适配加
以控制,如下图所示。
例如,有一个D i r e c t o r y B r o w s e r,它像前面一样使用Tr e e D i s p l a y。D i r e c t o r y B r o w s e r可能
为匹配Tr e e D i s p l a y和层次目录结构构造出一个较好的代理。在S m a l l t a l k或Objective C这样的
动态类型语言中,该方法只需要一个接口对适配器注册代理即可。然后Tr e e D i s p l a y简单地将
请求转发给代理对象。N E X T S T E P [ A d d 9 4 ]大量使用这种方法以减少子类化。
在C + +这样的静态类型语言中,需要一个代理的显式接口定义。我们将Tr e e D i s p l a y需要
的窄接口放入纯虚类Tr e e A c c e s s o r D e l e g a t e中,从而指定这样的一个接口。然后我们可以运用
继承机制将这个接口融合到我们所选择的代理中—这里我们选择D i r e c t o r y B r o w s e r。如果
D i r e c t o r y B r o w s e r没有父类我们将采用单继承,否则采用多继承。这种将类融合在一起的方法
相对于引入一个新的Tr e e D i s p l a y子类并单独实现它的操作的方法要容易一些。
c) 参数化的适配器通常在S m a l l t a l k中支持可插入适配器的方法是,用一个或多个模块
对适配器进行参数化。模块构造支持无子类化的适配。一个模块可以匹配一个请求,并且适
配器可以为每个请求存储一个模块。在本例中意味着, Tr e e D i s p l a y存储的一个模块用来将一
个节点转化成为一个G r a p h i c N o d e,另外一个模块用来存取一个节点的子节点。
例如,当对一个目录层次建立Tr e e D i s p l a y时,我们可以这样写:
如果你在一个类中创建接口适配,这种方法提供了另外一种选择,它相对于子类化方法
来说更方便一些。
10. 代码示例
对动机一节中例子,从类S h a p e和Te x t Vi e w开始,我们将给出类适配器和对象适配器实现
代码的简要框架。
9 6 设计模式:可复用面向对象软件的基础

S h a p e假定有一个边框,这个边框由它相对的两角定义。而Te x t Vi e w则由原点、宽度和高
度定义。S h a p 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对象。当用户操作
一个图形时, M a n i p u l a t o r对象知道如何驱动这个图形。Te x t Vi e w没有等同的操作。
Te x t S h a p e类是这些不同接口间的适配器。
类适配器采用多重继承适配接口。类适配器的关键是用一个分支继承接口,而用另外一
个分支继承接口的实现部分。通常C + +中作出这一区分的方法是:用公共方式继承接口;用
私有方式继承接口的实现。下面我们按照这种常规方法定义Te x t S h a p e适配器。
B o u n d i n g B o x操作对Te x t Vi e w的接口进行转换使之匹配S h a p e的接口。
I s E m p t y操作给出了在适配器实现过程中常用的一种方法:直接转发请求:
最后,我们定义C r e a t e M a n i p u l a t o r(Te x t Vi e w不支持该操作),假定我们已经实现了支持
Te x t S h a p e操作的类Te x t M a n i p u l a t o r。
第4章结构型模式9 7

C r e a t e M a n i p u l a t o r是一个Factory Method的实例。
对象适配器采用对象组合的方法将具有不同接口的类组合在一起。在该方法中,适配器
Te x t S h a p e维护一个指向Te x t Vi e w的指针。
Te x t S h a p e必须在构造器中对指向Te x t Vi e w实例的指针进行初始化,当它自身的操作被调
用时,它还必须对它的Te x t Vi e w对象调用相应的操作。在本例中,假设客户创建了Te x t Vi e w
对象并且将其传递给Te x t S h a p e的构造器:
C r e a t e M a n i p u l a t o r的实现代码与类适配器版本的实现代码一样,因为它的实现从零开始,
没有复用任何Te x t Vi e w已有的函数。
将这段代码与类适配器的相应代码进行比较,可以看出编写对象适配器代码相对麻烦一
些,但是它比较灵活。例如,客户仅需将Te x t Vi e w子类的一个实例传给Te x t S h a p e类的构造函
数,对象适配器版本的Te x t S h a p e就同样可以与Te x t Vi e w子类一起很好的工作。
11. 已知应用
意图一节的例子来自一个基于E T + + [ W G M 8 8 ]的绘图应用程序E T + + D r a w,E T + + D r a w通
过使用一个Te x t S h a p e适配器类的方式复用了E T + +中一些类,并将它们用于正文编辑。
I n t e r Vi e w 2 . 6为诸如s c r o l l b a r s、b u t t o n s和m e n u s的用户界面元素定义了一个抽象类
I n t e r a c t o r [ V L 8 8 ],它同时也为l i n e、c i r c l e、p o l y g o n和s p l i n e这样的结构化图形对象定义了一
个抽象类G r a p h i c s。I n t e r a c t o r和G r a p h i c s都有图形外观,但它们有着不同的接口和实现(它们
没有同一个父类),因此它们并不兼容。也就是说,你不能直接将一个结构化的图形对象嵌入
9 8 设计模式:可复用面向对象软件的基础

一个对话框中。
而I n t e r Vi e w 2 . 6定义了一个称为G r a p h i c B l o c k的对象适配器,它是I n t e r a c t o r的子类,包含
G r a p h i c类的一个实例。G r a p h i c B l o c k将G r a p h i c类的接口与I n t e r a c t o r类的接口进行匹配。
G r a p h i c B l o c k使得一个G r a p h i c的实例可以在I n t e r a c t o r结构中被显示、滚动和缩放。
可插入的适配器在O b j e c t Wo r k s / S m a l l t a l k [ P a r 9 0 ]中很常见。标准S m a l l t a l k为显示单个值的视图
定义了一个Va l u e M o d e l类。为访问这个值,Va l u e M o d e l定义了一个“v a l u e”和“v a l u e :”接口。这
些都是抽象方法。应用程序员用与特定领域相关的名字访问这个值,如“w i d t h”和“w i d t h :”,但
为了使特定领域相关的名字与Va l u e M o d e l的接口相匹配,他们不一定要生成Va l u e M o d e l的子类。
而O b j e c t Wo r k s / S m a l l t a l k包含了一个Va l u e M o d e l类的子类,称为P l u g g a b l e A d a p t o r。
P l u g g a b l e A d a p t o r对象可以将其他对象与Va l u e M o d e l的接口(“v a l u e”和“v a l u e :”)相匹配。它可
以用模块进行参数化,以便获取和设置所期望的值。P l u g g a b l e A d a p t o r在其内部使用这些模块以
实现“v a l u e”和“v a l u e :”接口,如下图所示。为语法上方便起见,P l u g g a b l e A d a p t o r也允许你直
接传递选择器的名字(例如“w i d t h”和“w i d t h :”),它自动将这些选择器转换为相应的模块。
另外一个来自O b j e c t Wo r k s / S m a l l t a l k的例子是Ta b l e A d a p t o r类,它可以将一个对象序列与
一个表格表示相匹配。这个表格在每行显示一个对象。客户用表格可以使用的消息集对
TableAdaptor 进行参数设置,从一个对象得到行属性。
在N e X T的A p p K i t [ A d d 9 4 ]中,一些类使用代理对象进行接口匹配。一个例子是类
N X B r o w s e r,它可以显示层次式数据列表。N X B r o w s e r类用一个代理对象存取并适配数据。
M a y e r的“Marriage of Convenience”[ M e y 8 8 ]是一种形式的类适配器。M a y e r描述了
F i x e d S t a c k类如何匹配一个A r r a y类的实现部分和一个S t a c k类的接口部分。结果是一个包含一
定数目项目的栈。
12. 相关模式
模式B r i d g e ( 4 . 2 )的结构与对象适配器类似,但是B r i d g e模式的出发点不同: B r i d g e目的是
将接口部分和实现部分分离,从而对它们可以较为容易也相对独立的加以改变。而A d a p t e r则
意味着改变一个已有对象的接口。
D e c o r a t o r ( 4 . 4 )模式增强了其他对象的功能而同时又不改变它的接口。因此d e c o r a t o r对应
用程序的透明性比适配器要好。结果是d e c o r a t o r支持递归组合,而纯粹使用适配器是不可能
实现这一点的。
模式P r o x y ( 4 . 7 )在不改变它的接口的条件下,为另一个对象定义了一个代理。
第4章结构型模式9 9

4.2 BRIDGE(桥接)—对象结构型模式
1. 意图
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
2. 别名
H a n d l e / B o d y
3. 动机
当一个抽象可能有多个实现时,通常用继承来协调它们。抽象类定义对该抽象的接口,
而具体的子类则用不同方式加以实现。但是此方法有时不够灵活。继承机制将抽象部分与它
的实现部分固定在一起,使得难以对抽象部分和实现部分独立地进行修改、扩充和重用。
让我们考虑在一个用户界面工具箱中,一个可移植的Wi n d o w抽象部分的实现。例如,这
一抽象部分应该允许用户开发一些在X Window System和I B M的Presentation Manager(PM)系
统中都可以使用的应用程序。运用继承机制,我们可以定义Wi n d o w抽象类和它的两个子类
XWi n d o w与P M Wi n d o w,由它们分别实现不同系统平台上的Wi n d o w界面。但是继承机制有两
个不足之处:
1) 扩展Wi n d o w抽象使之适用于不同种类的窗口或新的系统平台很不方便。假设有
Wi n d o w的一个子类I c o n Wi n d o w,它专门将Wi n d o w抽象用于图标处理。为了使I c o n Wi n d o w支
持两个系统平台,我们必须实现两个新类X I c o n Wi n d o w和P M I c o n Wi n d o w,更为糟糕的是,
我们不得不为每一种类型的窗口都定义两个类。而为了支持第三个系统平台我们还必须为每
一种窗口定义一个新的Wi n d o w子类,如下图所示。
2) 继承机制使得客户代码与平台相关。每当客户创建一个窗口时,必须要实例化一个具
体的类,这个类有特定的实现部分。例如,创建X w i n d o w对象会将Wi n d o w抽象与X Wi n d o w
的实现部分绑定起来,这使得客户程序依赖于X Wi n d o w的实现部分。这将使得很难将客户代
码移植到其他平台上去。
客户在创建窗口时应该不涉及到其具体实现部分。仅仅是窗口的实现部分依赖于应用运
行的平台。这样客户代码在创建窗口时就不应涉及到特定的平台。
B r i d g e模式解决以上问题的方法是,将Wi n d o w抽象和它的实现部分分别放在独立的类层
次结构中。其中一个类层次结构针对窗口接口( Wi n d o w、I c o n Wi n d o w、Tr a n s i e n t Wi n d o w),
另外一个独立的类层次结构针对平台相关的窗口实现部分,这个类层次结构的根类为
Wi n d o w I m p。例如X w i n d o w I m p子类提供了一个基于X Wi n d o w系统的实现,如下页上图所示。
对Wi n d o w子类的所有操作都是用Wi n d o w I m p接口中的抽象操作实现的。这就将窗口的抽
象与系统平台相关的实现部分分离开来。因此,我们将Wi n d o w与Wi n d o w I m p之间的关系称之
为桥接,因为它在抽象类与它的实现之间起到了桥梁作用,使它们可以独立地变化。
1 0 0 设计模式:可复用面向对象软件的基础

4. 适用性
以下一些情况使用B r i d g e模式:
• 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,
在程序运行时刻实现部分应可以被选择或者切换。
• 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时B r i d g e模式使你
可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
• 对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
• (C + +)你想对客户完全隐藏抽象的实现部分。在C + +中,类的表示在类接口中是可见
的。
• 正如在意图一节的第一个类图中所示的那样,有许多类要生成。这样一种类层次结构说
明你必须将一个对象分解成两个部分。R u m b a u g h称这种类层次结构为“嵌套的普化”
(nested generalizations)。
• 你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。
一个简单的例子便是C o p l i e n的S t r i n g类[ C o p 9 2 ],在这个类中多个对象可以共享同一个字
符串表示( S t r i n g R e p)。
5. 结构
第4章结构型模式1 0 1

6. 参与者
• Abstraction (Wi n d o w )
— 定义抽象类的接口。
— 维护一个指向I m p l e m e n t o r类型对象的指针。
• RefinedAbstraction (IconWi n d o w )
— 扩充由A b s t r a c t i o n定义的接口。
• Implementor (Wi n d o w I m p )
— 定义实现类的接口,该接口不一定要与A b s t r a c t i o n的接口完全一致;事实上这两个
接口可以完全不同。一般来讲, I m p l e m e n t o r接口仅提供基本操作,而A b s t r a c t i o n则
定义了基于这些基本操作的较高层次的操作。
• ConcreteImplementor (XwindowImp, PMWi n d o w I m p )
— 实现I m p l e m e n t o r接口并定义它的具体实现。
7. 协作
• Abstraction将c l i e n t的请求转发给它的I m p l e m e n t o r对象。
8. 效果
B r i d g e模式有以下一些优点:
1) 分离接口及其实现部分一个实现未必不变地绑定在一个接口上。抽象类的实现可以
在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。
将A b s t r a c t i o n与I m p l e m e n t o r分离有助于降低对实现部分编译时刻的依赖性,当改变一个
实现类时,并不需要重新编译A b s t r a c t i o n类和它的客户程序。为了保证一个类库的不同版本
之间的二进制兼容性,一定要有这个性质。
另外,接口与实现分离有助于分层,从而产生更好的结构化系统,系统的高层部分仅需
知道A b s t r a c t i o n和I m p l e m e n t o r即可。
2) 提高可扩充性你可以独立地对A b s t r a c t i o n和I m p l e m e n t o r层次结构进行扩充。
3 ) 实现细节对客户透明你可以对客户隐藏实现细节,例如共享I m p l e m e n t o r对象以及相
应的引用计数机制(如果有的话)。
9. 实现
使用B r i d g e模式时需要注意以下一些问题:
1) 仅有一个Implementor 在仅有一个实现的时候,没有必要创建一个抽象的I m p l e m e n t o r
类。这是B r i d g e模式的退化情况;在A b s t r a c t i o n与I m p l e m e n t o r之间有一种一对一的关系。尽
管如此,当你希望改变一个类的实现不会影响已有的客户程序时,模式的分离机制还是非常
有用的—也就是说,不必重新编译它们,仅需重新连接即可。
C a r o l a n [ C a r 8 9 ]用“常露齿嘻笑的猫”(Cheshire Cat)描述这一分离机制。在C + +中,
I m p l e m e n t o r类的类接口可以在一个私有的头文件中定义,这个文件不提供给客户。这样你就
对客户彻底隐藏了一个类的实现部分。
2) 创建正确的I m p l e m e n t o r对象当存在多个I m p l e m e n t o r类的时候,你应该用何种方法,
在何时何处确定创建哪一个I m p l e m e n t o r类呢?
如果A b s t r a c t i o n知道所有的C o n c r e t e I m p l e m e n t o r类,它就可以在它的构造器中对其中的
一个类进行实例化,它可以通过传递给构造器的参数确定实例化哪一个类。例如,如果一个
1 0 2 设计模式:可复用面向对象软件的基础

c o l l e c t i o n类支持多重实现,就可以根据c o l l e c t i o n的大小决定实例化哪一个类。链表的实现可
以用于较小的c o l l e c t i o n类,而h a s h表则可用于较大的c o l l e c t i o n类。
另外一种方法是首先选择一个缺省的实现,然后根据需要改变这个实现。例如,如果一
个c o l l e c t i o n的大小超出了一定的阈值时,它将会切换它的实现,使之更适用于表目较多的
c o l l e c t i o n。
也可以代理给另一个对象,由它一次决定。在Wi n d o w / Wi n d o w I m p的例子中,我们可以
引入一个f a c t o r y对象(参见Abstract Factory(3.1)),该对象的唯一职责就是封装系统平台的细
节。这个对象知道应该为所用的平台创建何种类型的Wi n d o w I m p对象;Wi n d o w仅需向它请求
一个Wi n d o w I m p,而它会返回正确类型的Wi n d o w I m p对象。这种方法的优点是Abstraction 类
不和任何一个I m p l e m e n t o r类直接耦合。
3 ) 共享I m p l e m e n t o r对象C o p l i e n阐明了如何用C + +中常用的H a n d l e / B o d y方法在多个对象
间共享一些实现[ C o p 9 2 ]。其中B o d y有一个对象引用计数器, H a n d l e对它进行增减操作。将共
享程序体赋给句柄的代码一般具有以下形式:
4) 采用多重继承机制在C + +中可以使用多重继承机制将抽象接口和它的实现部分结合起
来[ M a r 9 1 ] 。例如,一个类可以用p u b l i c 方式继承A b s t r a c t i o n而以p r i v a t e 方式继承
C o n c r e t e I m p l e m e n t o r。但是由于这种方法依赖于静态继承,它将实现部分与接口固定不变的
绑定在一起。因此不可能使用多重继承的方法实现真正的B r i d g e模式—至少用C + +不行。
10. 代码示例
下面的C + +代码实现了意图一节中Wi n d o w / Wi n d w o I m p的例子,其中Wi n d o w类为客户应
用程序定义了窗口抽象类:
第4章结构型模式1 0 3

Wi n d o w维护一个对Wi n d o w I m p的引用,Wi n d o w I m p抽象类定义了一个对底层窗口系统的
接口。
Wi n d o w的子类定义了应用程序可能用到的不同类型的窗口,如应用窗口、图标、对话框
临时窗口以及工具箱的移动面板等等。
例如A p p l i c a t i o n Wi n d o w类将实现D r a w C o n t e n t s操作以绘制它所存储的Vi e w实例:
I c o n Wi n d o w中存储了它所显示的图标对应的位图名. . .
. . .并且实现D r a w C o n t e n t s操作将这个位图绘制在窗口上:
1 0 4 设计模式:可复用面向对象软件的基础

我们还可以定义许多其他类型的Window 类,例如Tr a n s i e n t Wi n d o w在与客户对话时由一
个窗口创建,它可能要和这个创建它的窗口进行通信; P a l e t t e Window 总是在其他窗口之上;
I c o n D o c k Wi n d o w拥有一些I c o n Wi n d o w,并且由它负责将它们排列整齐。
Wi n d o w的操作由Wi n d o w I m p的接口定义。例如,在调用Wi n d o w I m p操作在窗口中绘制矩
形之前,D r a w R e c t必须从它的两个P o i n t参数中提取四个坐标值:
具体的Wi n d o w I m p子类可支持不同的窗口系统, X w i n d o w I m p子类支持X Wi n d o w窗口系
统:
对于Presentation Manager (PM),我们定义P M Wi n d o w I m p类:
这些子类用窗口系统的基本操作实现Wi n d o w I m p操作,例如,对于X 窗口系统这样实现
D e v i c e R e c t:
P M的实现部分可能象下面这样:
第4章结构型模式1 0 5

那么一个窗口怎样得到正确的Wi n d o w I m p子类的实例呢?在本例我们假设Window 类具
有这个职责,它的G e t Wi n d o w I m p操作负责从一个抽象工厂(参见Abstract Factory(3.1)模式)
得到正确的实例,这个抽象工厂封装了所有窗口系统的细节。
Wi n d o w S y s t e m F a c t o r y : : I n s t a n c e ( )函数返回一个抽象工厂,该工厂负责处理所有与特定窗
口系统相关的对象。为简化起见,我们将它创建一个单件( S i n g l e t o n),允许Wi n d o w类直接
访问这个工厂。
11. 已知应用
上面的Wi n d o w实例来自于E T + + [ W G M 8 8 ]。在E T + +中,Wi n d o w I m p称为“Wi n d o w P o r t”,
它有X Wi n d o w P o r t和S u n Wi n d o w P o r t这样一些子类。Wi n d o w对象请求一个称为
“Wi n d o w S y s t e m”的抽象工厂创建相应的I m p l e m e n t o r对象。Wi n d o w S y s t e m提供了一个接口
用于创建一些与特定平台相关的对象,例如字体、光标、位图等。
E T + +的Wi n d o w / Wi n d o w P o r t设计扩展了B r i d g e模式,因为Wi n d o w P o r t保留了一个指回
Wi n d o w的指针。Wi n d o w P o r t的I m p l e m e n t o r类用这个指针通知Wi n d o w对象发生了一些与
Wi n d o w P o r t相关的事件:例如输入事件的到来,窗口调整大小等。
Coplien[Cop92] 和S t r o u s t r u p [ S t r 9 1 ]都提及H a n d l e类并给出了一些例子。这些例子集中处
理一些内存管理问题,例如共享字符串表达式以及支持大小可变的对象等。我们主要关心它
怎样支持对一个抽象和它的实现进行独立地扩展。
l i b g + + [ L e a 8 8 ]类库定义了一些类用于实现公共的数据结构,例如S e t、L i n k e d S e t、
H a s h S e t、L i n k e d L i s t和H a s h Ta b l e。S e t是一个抽象类,它定义了一组抽象接口,而L i n k e d L i s t
和H a s h Ta b l e则分别是链表和h a s h表的具体实现。L i n k e d S e t和H a s h S e t是S e t的实现者,它们桥
接了S e t和它们具体所对应的L i n k e d L i s t和H a s h Table. 这是一种退化的桥接模式,因为没有抽象
I m p l e m e n t o r类。
1 0 6 设计模式:可复用面向对象软件的基础

N e X T s AppKit[Add94]在图象生成和显示中使用了B r i d g e模式。一个图象可以有多种不
同的表示方式,一个图象的最佳显示方式取决于显示设备的特性,特别是它的色彩数目和分
辨率。如果没有A p p K i t的帮助,每一个应用程序中应用开发者都要确定在不同的情况下应该
使用哪一种实现方法。
为了减轻开发者的负担, A p p K i t提供了N X I m a g e / N X I m a g e R e p桥接。N T I m a g e定义了图
象处理的接口,而图象接口的实现部分则定义在独立的N X I m a g e R e p类层次中,这个类层次包
含了多个子类,如NXEPSImageRep, NXCachedImageRep和N X B i t M a p I m a g e R e p等。N X I m a g e
维护一个指针,指向一个或多个N X I m a g e R e p对象。如果有多个图象实现, N X I m a g e会选择一
个最适合当前显示设备的图象实现。必要时N X I m a g e还可以将一个实现转换成另一个实现。
这个B r i d g e模式变种很有趣的地方是: N X I m a g e能同时存储多个N X I m a g e R e p实现。
12. 相关模式
Abstract Factory(3.1) 模式可以用来创建和配置一个特定的B r i d g e模式。
Adapter(4.1) 模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然
而,B r i d g e模式则是在系统开始时就被使用,它使得抽象接口和实现部分可以独立进行改变。
4.3 COMPOSITE(组合)—对象结构型模式
1. 意图
将对象组合成树形结构以表示“部分-整体”的层次结构。C o m p o s i t e使得用户对单个对象
和组合对象的使用具有一致性。
2. 动机
在绘图编辑器和图形捕捉系统这样的图形应用程序中,用户可以使用简单的组件创建复
杂的图表。用户可以组合多个简单组件以形成一些较大的组件,这些组件又可以组合成更大
的组件。一个简单的实现方法是为Te x t和L i n e这样的图元定义一些类,另外定义一些类作为这
些图元的容器类( C o n t a i n e r )。
然而这种方法存在一个问题:使用这些类的代码必须区别对待图元对象与容器对象,而
实际上大多数情况下用户认为它们是一样的。对这些类区别使用,使得程序更加复杂。
C o m p o s i t e模式描述了如何使用递归组合,使得用户不必对这些类进行区别,如下图所示。
Composite 模式的关键是一个抽象类,它既可以代表图元,又可以代表图元的容器。在图
形系统中的这个类就是G r a p h i c,它声明一些与特定图形对象相关的操作,例如D r a w。同时它
第4章结构型模式1 0 7

也声明了所有的组合对象共享的一些操作,例如一些操作用于访问和管理它的子部件。
子类L i n e、R e c t a n g l e和Te x t(参见前面的类图)定义了一些图元对象,这些类实现D r a w,
分别用于绘制直线、矩形和正文。由于图元都没有子图形,因此它们都不执行与子类有关的
操作。
P i c t u r e类定义了一个Graphic 对象的聚合。Picture 的D r a w操作是通过对它的子部件调用
D r a w实现的, P i c t u r e还用这种方法实现了一些与其子部件相关的操作。由于P i c t u r e接口与
G r a p h i c接口是一致的,因此P i c t u r e对象可以递归地组合其他P i c t u r e对象。
下图是一个典型的由递归组合的G r a p h i c对象组成的组合对象结构。
3. 适用性
以下情况使用C o m p o s i t e模式:
• 你想表示对象的部分-整体层次结构。
• 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对
象。
4. 结构
典型的C o m p o s i t e对象结构如下图所示。
1 0 8 设计模式:可复用面向对象软件的基础

5. 参与者
• Component (Graphic)
— 为组合中的对象声明接口。
— 在适当的情况下,实现所有类共有接口的缺省行为。
— 声明一个接口用于访问和管理C o m p o n e n t的子组件。
—(可选)在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
• Leaf (Rectangle、L i n e、Te x t等)
— 在组合中表示叶节点对象,叶节点没有子节点。
— 在组合中定义图元对象的行为。
• Composite (Picture)
— 定义有子部件的那些部件的行为。
— 存储子部件。
— 在C o m p o n e n t接口中实现与子部件有关的操作。
• Client
— 通过C o m p o n e n t接口操纵组合部件的对象。
6. 协作
• 用户使用C o m p o n e n t类接口与组合结构中的对象进行交互。如果接收者是一个叶节点,则
直接处理请求。如果接收者是Composite, 它通常将请求发送给它的子部件,在转发请求
之前与/或之后可能执行一些辅助操作。
7. 效果
C o m p o s i t e模式
• 定义了包含基本对象和组合对象的类层次结构基本对象可以被组合成更复杂的组合对
象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本
对象的地方都可以使用组合对象。
• 简化客户代码客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)
处理的是一个叶节点还是一个组合组件。这就简化了客户代码, 因为在定义组合的那些
类中不需要写一些充斥着选择语句的函数。
• 使得更容易增加新类型的组件新定义的C o m p o s i t e或L e a f子类自动地与已有的结构和客
户代码一起工作,客户程序不需因新的C o m p o n e n t类而改变。
• 使你的设计变得更加一般化容易增加新组件也会产生一些问题,那就是很难限制组合
中的组件。有时你希望一个组合只能有某些特定的组件。使用C o m p o s i t e时,你不能依
赖类型系统施加这些约束,而必须在运行时刻进行检查。
8. 实现
我们在实现C o m p o s i t e模式时需要考虑以下几个问题:
1 ) 显式的父部件引用保持从子部件到父部件的引用能简化组合结构的遍历和管理。父部
件引用可以简化结构的上移和组件的删除,同时父部件引用也支持Chain of Responsibility(5.2)
模式。
通常在C o m p o n e n t类中定义父部件引用。L e a f和C o m p o s i t e类可以继承这个引用以及管理
这个引用的那些操作。
第4章结构型模式1 0 9

对于父部件引用,必须维护一个不变式,即一个组合的所有子节点以这个组合为父节点,
而反之该组合以这些节点为子节点。保证这一点最容易的办法是,仅当在一个组合中增加或
删除一个组件时,才改变这个组件的父部件。如果能在C o m p o s i t e类的Add 和R e m o v e操作中
实现这种方法,那么所有的子类都可以继承这一方法,并且将自动维护这一不变式。
2 ) 共享组件共享组件是很有用的,比如它可以减少对存贮的需求。但是当一个组件只
有一个父部件时,很难共享组件。
一个可行的解决办法是为子部件存贮多个父部件,但当一个请求在结构中向上传递时,
这种方法会导致多义性。F l y w e i g h t ( 4 . 6 )模式讨论了如何修改设计以避免将父部件存贮在一起
的方法。如果子部件可以将一些状态(或是所有的状态)存储在外部,从而不需要向父部件发送
请求,那么这种方法是可行的。
3) 最大化C o m p o n e n t接口C o m p o s i t e模式的目的之一是使得用户不知道他们正在使用的
具体的Leaf 和C o m p o s i t e类。为了达到这一目的, C o m p o s i t e类应为Leaf 和C o m p o s i t e类尽可能
多定义一些公共操作。C o m p o s i t e类通常为这些操作提供缺省的实现,而Leaf 和C o m p o s i t e子
类可以对它们进行重定义。
然而,这个目标有时可能会与类层次结构设计原则相冲突,该原则规定:一个类只能定
义那些对它的子类有意义的操作。有许多C o m p o n e n t所支持的操作对L e a f类似乎没有什么意义,
那么C o m p o n e n t怎样为它们提供一个缺省的操作呢?
有时一点创造性可以使得一个看起来仅对C o m p o s i t e 才有意义的操作,将它移入
C o m p o n e n t类中,就会对所有的C o m p o n e n t都适用。例如,访问子节点的接口是C o m p o s i t e类
的一个基本组成部分,但对L e a f类来说并不必要。但是如果我们把一个L e a f看成一个没有子
节点的Component, 就可以为在C o m p o n e n t类中定义一个缺省的操作,用于对子节点进行访问,
这个缺省的操作不返回任何一个子节点。Leaf 类可以使用缺省的实现,而C o m p o s i t e类则会重
新实现这个操作以返回它们的子类。
管理子部件的操作比较复杂,我们将在下一项中予以讨论。
4) 声明管理子部件的操作虽然C o m p o s i t e类实现了Add 和R e m o v e操作用于管理子部件,
但在C o m p o s i t e模式中一个重要的问题是:在C o m p o s i t e类层次结构中哪一些类声明这些操作。
我们是应该在C o m p o n e n t中声明这些操作,并使这些操作对L e a f类有意义呢,还是只应该在
C o m p o s i t e和它的子类中声明并定义这些操作呢?
这需要在安全性和透明性之间做出权衡选择。
• 在类层次结构的根部定义子节点管理接口的方法具有良好的透明性,因为你可以一致地
使用所有的组件,但是这一方法是以安全性为代价的,因为客户有可能会做一些无意义
的事情,例如在Leaf 中增加和删除对象等。
• 在C o m p o s i t e类中定义管理子部件的方法具有良好的安全性,因为在象C + +这样的静态
类型语言中,在编译时任何从Leaf 中增加或删除对象的尝试都将被发现。但是这又损失
了透明性,因为Leaf 和C o m p o s i t e具有不同的接口。
在这一模式中,相对于安全性,我们比较强调透明性。如果你选择了安全性,有时你可
能会丢失类型信息,并且不得不将一个组件转换成一个组合。这样的类型转换必定不是类型
安全的。
一种办法是在C o m p o n e n t类中声明一个操作Composite* GetComposite()。C o m p o n e n t提供
1 1 0 设计模式:可复用面向对象软件的基础

了一个返回空指针的缺省操作。C o m p o s i t e类重新定义这个操作并通过t h i s指针返回它自身。
GetComposite 允许你查询一个组件看它是否是一个组合,你可以对返回的组合安全地执
行Add 和R e m o v e操作。
你可使用C++ 中的d y n a m i c _ c a s t结构对C o m p o s i t e做相似的试验。
当然,这里的问题是我们对所有的组件的处理并不一致。在进行适当的动作之前,我们
必须检测不同的类型。
提供透明性的唯一方法是在C o m p o n e n t中定义缺省Add 和R e m o v e操作。这又带来了一个
新的问题: C o m p o n e n t : : A d d 的实现不可避免地会有失败的可能性。你可以不让
C o m p o n e n t : : A d d做任何事情,但这就忽略了一个很重要的问题:企图向叶节点中增加一些东
西时可能会引入错误。这时A d d操作会产生垃圾。你可以让A d d操作删除它的参数,但可能客
户并不希望这样。
如果该组件不允许有子部件,或者R e m o v e的参数不是该组件的子节点时,通常最好使用
缺省方式(可能是产生一个异常)处理A d d和R e m o v e的失败。
另一个办法是对“删除”的含义作一些改变。如果该组件有一个父部件引用,我们可重
新定义Component :: Remove,在它的父组件中删除掉这个组件。然而,对应的A d d操作仍然没
有合理的解释。
5) Component是否应该实现一个C o m p o n e n t列表你可能希望在C o m p o n e n t类中将子节点
集合定义为一个实例变量,而这个C o m p o n e n t类中也声明了一些操作对子节点进行访问和管
第4章结构型模式1 1 1

理。但是在基类中存放子类指针,对叶节点来说会导致空间浪费,因为叶节点根本没有子节
点。只有当该结构中子类数目相对较少时,才值得使用这种方法。
6) 子部件排序许多设计指定了C o m p o s i t e的子部件顺序。在前面的G r a p h i c s例子中,排
序可能表示了从前至后的顺序。如果C o m p o s i t e表示语法分析树, C o m p o s i t e子部件的顺序必
须反映程序结构,而组合语句就是这样一些C o m p o s i t e的实例。
如果需要考虑子节点的顺序时,必须仔细地设计对子节点的访问和管理接口,以便管理
子节点序列。I t e r a t o r模式( 5 . 4 )可以在这方面给予一些定的指导。
7) 使用高速缓冲存贮改善性能如果你需要对组合进行频繁的遍历或查找, C o m p o s i t e类
可以缓冲存储对它的子节点进行遍历或查找的相关信息。C o m p o s i t e可以缓冲存储实际结果或
者仅仅是一些用于缩短遍历或查询长度的信息。例如,动机一节的例子中P i c t u r e类能高速缓
冲存贮其子部件的边界框,在绘图或选择期间,当子部件在当前窗口中不可见时,这个边界
框使得P i c t u r e不需要再进行绘图或选择。
一个组件发生变化时,它的父部件原先缓冲存贮的信息也变得无效。在组件知道其父部
件时,这种方法最为有效。因此,如果你使用高速缓冲存贮,你需要定义一个接口来通知组
合组件它们所缓冲存贮的信息无效。
8) 应该由谁删除Component 在没有垃圾回收机制的语言中,当一个C o m p o s i t e被销毁时,
通常最好由C o m p o s i t e负责删除其子节点。但有一种情况除外,即L e a f对象不会改变,因此可
以被共享。
9) 存贮组件最好用哪一种数据结构C o m p o s i t e可使用多种数据结构存贮它们的子节点,
包括连接列表、树、数组和h a s h表。数据结构的选择取决于效率。事实上,使用通用数据结
构根本没有必要。有时对每个子节点, C o m p o s i t e 都有一个变量与之对应,这就要求
C o m p o s i t e的每个子类都要实现自己的管理接口。参见I n t e r p r e t e r ( 5 . 3 )模式中的例子。
9. 代码示例
计算机和立体声组合音响这样的设备经常被组装成部分-整体层次结构或者是容器层次
结构。例如,底盘可包含驱动装置和平面板,总线含有多个插件,机柜包括底盘、总线等。
这种结构可以很自然地用C o m p o s i t e模式进行模拟。
E q u i p m e n t类为在部分-整体层次结构中的所有设备定义了一个接口。
1 1 2 设计模式:可复用面向对象软件的基础

Equipment 声明一些操作返回一个设备的属性,例如它的能量消耗和价格。子类为指定的
设备实现这些操作, E q u i p m e n t还声明了一个C r e a t e I t e r a t o r操作,该操作为访问它的零件返回
一个I t e r a t o r(参见附录C)。这个操作的缺省实现返回一个N u l l I t e r a t o r,它在空集上叠代。
Equipment 的子类包括表示磁盘驱动器、集成电路和开关的L e a f类:
CompositeEquipment 是包含其他设备的基类,它也是E q u i p m e n t的子类。
C o m p o s i t e E q u i p m e n t为访问和管理子设备定义了一些操作。操作Add 和R e m o v e从存储在
_ e q u i p m e n t成员变量中的设备列表中插入并删除设备。操作C r e a t e I t e r a t o r返回一个迭代器
(L i s t I t e r a t o r的一个实例)遍历这个列表。
N e t P r i c e的缺省实现使用CreateIterator 来累加子设备的实际价格。
现在我们将计算机的底盘表示为C o m p o s i t e E q u i p m e n t的子类C h a s s i s。C h a s s i s从
C o m p o s i t e E q u i p m e n t继承了与子类有关的那些操作。
第4章结构型模式1 1 3

用完I t e r a t o r时,很容易忘记删除它。I t e r a t o r模式描述了如何处理这类问题。
我们可用相似的方式定义其他设备容器,如C a b i n e t和B u s。这样我们就得到了组装一台
(非常简单)个人计算机所需的所有设备。
10. 已知应用
几乎在所有面向对象的系统中都有Composite 模式的应用实例。在S m a l l t a l k中的
M o d e l / Vi e w / C o n t r o l l e r [ K P 8 8 ]结构中,原始Vi e w类就是一个Composite, 几乎每个用户界面工
具箱或框架都遵循这些步骤,其中包括ET++ ( 用V O b j e c t s [ W G M 8 8 ] )和I n t e r Vi e w s ( S t y l e
[ L C I + 9 2 ] , G r a p h i c s [ V L 8 8 ]和G l y p h s [ C L 9 0 ] )。很有趣的是Model /Vi e w / C o n t r o l l e r中的原始Vi e w
有一组子视图;换句话说, View 既是Component 类,又是C o m p o s i t e类。4 . 0版的S m a l l t a l k - 8 0
用Vi s u a l C o m p o n e n t类修改了M o d e l / Vi e w / C o n t r o l l e r, Vi s u a l C o m p o n e n t类含有子类Vi e w和
C o m p o s i t e Vi e w。
RTL Smalltalk 编译器框架[ J M L 9 2 ]大量地使用了C o m p o s i t e模式。RTLExpression 是一个
对应于语法分析树的C o m p o n e n t 类。它有一些子类,例如B i n a r y E x p r e s s i o n ,而
B i n a r y E x p r e s s i o n包含子RT L E x p r e s s i o n对象。这些类为语法分析树定义了一个组合结构。
R e g i s t e r Tr a n s f e r是一个用于程序的中间Single Static Assignment(SSA)形式的Component 类。
R e g i s t e r Tr a n s f e r的L e a f子类定义了一些不同的静态赋值形式,例如:
• 基本赋值,在两个寄存器上执行操作并且将结果放入第三个寄存器中。
• 具有源寄存器但无目标寄存器的赋值,这说明是在例程返回后使用该寄存器。
• 具有目标寄存器但无源寄存器的赋值,这说明是在例程开始之前分配目标寄存器。
另一个子类R e g i s t e r Tr a n s f e r S e t,是一个C o m p o s i t e类,表示一次改变几个寄存器的赋值。
这种模式的另一个例子出现在财经应用领域,在这一领域中,一个资产组合聚合多个单
个资产。为了支持复杂的资产聚合,资产组合可以用一个C o m p o s i t e类实现,这个C o m p o s i t e
类与单个资产的接口一致[ B E 9 3 ]。
C o m m a n d(5 . 2)模式描述了如何用一个MacroCommand Composite类组成一些C o m m a n d
对象,并对它们进行排序。
11. 相关模式
通常部件-父部件连接用于Responsibility of Chain(5.1)模式。
D e c o r a t o r(4 . 4)模式经常与C o m p o s i t e模式一起使用。当装饰和组合一起使用时,它们
通常有一个公共的父类。因此装饰必须支持具有A d d、R e m o v e和GetChild 操作的C o m p o n e n t
1 1 4 设计模式:可复用面向对象软件的基础

接口。
F l y w e i g h t ( 4 . 6 )让你共享组件,但不再能引用他们的父部件。
I t e r t o r ( 5 . 4 )可用来遍历C o m p o s i t e。
Vi s i t o r ( 5 . 11 )将本来应该分布在C o m p o s i t e和L e a f类中的操作和行为局部化。
4.4 DECORATOR(装饰)—对象结构型模式
1. 意图
动态地给一个对象添加一些额外的职责。就增加功能来说, D e c o r a t o r模式相比生成子类
更为灵活。
2. 别名
包装器Wr a p p e r
3. 动机
有时我们希望给某个对象而不是整个类添加一些功能。例如,一个图形用户界面工具箱
允许你对任意一个用户界面组件添加一些特性,例如边框,或是一些行为,例如窗口滚动。
使用继承机制是添加功能的一种有效途径,从其他类继承过来的边框特性可以被多个子
类的实例所使用。但这种方法不够灵活,因为边框的选择是静态的,用户不能控制对组件加
边框的方式和时机。
一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌
入的对象为装饰。这个装饰与它所装饰的组件接口一致,因此它对使用该组件的客户透明。
它将客户请求转发给该组件,并且可能在转发前后执行一些额外的动作(例如画一个边框)。
透明性使得你可以递归的嵌套多个装饰,从而可以添加任意多的功能,如下图所示。
例如,假定有一个对象Te x t Vi e w,它可以在窗口中显示正文。缺省的Te x t Vi e w没有滚动
条,因为我们可能有时并不需要滚动条。当需要滚动条时,我们可以用S c r o l l D e c o r a t o r添加滚
动条。如果我们还想在Te x t Vi e w周围添加一个粗黑边框,可以使用B o r d e r D e c o r a t o r添加。因
此只要简单地将这些装饰和Te x t Vi e w进行组合,就可以达到预期的效果。
下面的对象图展示了如何将一个Te x t Vi e w对象与B o r d e r D e c o r a t o r以及S c r o l l D e c o r a t o r对象
组装起来产生一个具有边框和滚动条的文本显示窗口。
第4章结构型模式1 1 5

S c r o l l D e c o r a t o r和BorderDecorator 类是D e c o r a t o r类的子类。D e c o r a t o r类是一个可视组件
的抽象类,用于装饰其他可视组件,如下图所示。
Vi s u a l C o m p o n e n t是一个描述可视对象的抽象类,它定义了绘制和事件处理的接口。注意
D e c o r a t o r类怎样将绘制请求简单地发送给它的组件,以及D e c o r a t o r的子类如何扩展这个操作。
D e c o r a t o r的子类为特定功能可以自由地添加一些操作。例如,如果其他对象知道界面中
恰好有一个S c r o l l D e c o r a t o r对象,这些对象就可以用S c r o l l D e c o r a t o r对象的S c r o l l To操作滚动这
个界面。这个模式中有一点很重要,它使得在Vi s u a l C o m p o n e n t可以出现的任何地方都可以有
装饰。因此,客户通常不会感觉到装饰过的组件与未装饰组件之间的差异,也不会与装饰产
生任何依赖关系。
4. 适用性
以下情况使用D e c o r a t o r模式
• 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
• 处理那些可以撤消的职责。
• 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持
每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类
定义被隐藏,或类定义不能用于生成子类。
5. 结构
1 1 6 设计模式:可复用面向对象软件的基础

6. 参与者
• Component ( Vi s u a l C o m p o n e n t )
— 定义一个对象接口,可以给这些对象动态地添加职责。
• C o n c r e t e C o m p o n e n t ( Te x t Vi e w )
— 定义一个对象,可以给这个对象添加一些职责。
• D e c o r a t o r
— 维持一个指向C o m p o n e n t对象的指针,并定义一个与C o m p o n e n t接口一致的接口。
• C o n c r e t e D e c o r a t o r ( B o r d e r D e c o r a t o r, ScrollDecorator)
— 向组件添加职责。
7. 协作
• D e c o r a t o r将请求转发给它的C o m p o n e n t对象,并有可能在转发请求前后执行一些附加的
动作。
8. 效果
D e c o r a t o r模式至少有两个主要优点和两个缺点:
1) 比静态继承更灵活与对象的静态继承(多重继承)相比, D e c o r a t o r模式提供了更加
灵活的向对象添加职责的方式。可以用添加和分离的方法,用装饰在运行时刻增加和删除职
责。相比之下,继承机制要求为每个添加的职责创建一个新的子类(例如, B o r d e r S c r o l l a b l e
Te x t Vi e w, BorderedTe x t Vi e w)。这会产生许多新的类,并且会增加系统的复杂度。此外,为一
个特定的C o m p o n e n t类提供多个不同的D e c o r a t o r类,这就使得你可以对一些职责进行混合和
匹配。
使用D e c o r a t o r模式可以很容易地重复添加一个特性,例如在Te x t Vi e w上添加双边框时,
仅需将添加两个B o r d e r D e c o r a t o r即可。而两次继承B o r d e r类则极容易出错的。
2) 避免在层次结构高层的类有太多的特征D e c o r a t o r模式提供了一种“即用即付”的方
法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可
以定义一个简单的类,并且用D e c o r a t o r类给它逐渐地添加功能。可以从简单的部件组合出复
杂的功能。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于D e c o r a t o r
所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的D e c o r a t o r。扩展一个复杂类的
时候,很可能会暴露与添加的职责无关的细节。
3) Decorator与它的C o m p o n e n t不一样D e c o r a t o r是一个透明的包装。如果我们从对象标
识的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖
对象标识。
4) 有许多小对象采用D e c o r a t o r模式进行系统设计往往会产生许多看上去类似的小对象,
这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不
同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,

抱歉!评论已关闭.