第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模式进行系统设计往往会产生许多看上去类似的小对象,
这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不
同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,
排错也很困难。
9. 实现
使用D e c o r a t o r模式时应注意以下几点:
1) 接口的一致性装饰对象的接口必须与它所装饰的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类必须有一个公共的父类(至少在C + +中如此)。
第4章结构型模式1 1 7
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向
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中。
3) 保持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类会变得过于复杂和庞大,因
而难以大量使用。赋予C o m p o n e n t太多的功能也使得,具体的子类有一些它们并不需要的功
能的可能性大大增加。
4) 改变对象外壳与改变对象内核我们可以将D e c o r a t o r看作一个对象的外壳,它可以改
变这个对象的行为。另外一种方法是改变对象的内核。例如, S t r a t e g y ( 5 . 9 )模式就是一个用于
改变内核的很好的模式。
当C o m p o n e n t类原本就很庞大时,使用D e c o r a t o r模式代价太高, S t r a t e g y模式相对更好一
些。在S t r a t e g y模式中,组件将它的一些行为转发给一个独立的策略对象,我们可以替换
s t r a t e g y对象,从而改变或扩充组件的功能。
例如我们可以将组件绘制边界的功能延迟到一个独立的B o r d e r对象中,这样就可以支持
不同的边界风格。这个B o r d e r对象是一个S t r a t e g y对象,它封装了边界绘制策略。我们可以将
策略的数目从一个扩充为任意多个,这样产生的效果与对装饰进行递归嵌套是一样的。
在M a c A p p 3 . 0 [ A p p 8 9 ]和B e d r o c k [ S y m 9 3 a ]中,绘图组件(称之为“视图”)有一个“装饰”
( a d o r n e r )对象列表,这些对象可用来给一个视图组件添加一些装饰,例如边框。如果给一个
视图添加了一些装饰,就可以用这些装饰对这个视图进行一些额外的修饰。由于Vi e w类过于
庞大, M a c A p p和B e d r o c k必须使用这种方法。仅为添加一个边框就使用一个完整的Vi e w,代
价太高。
由于D e c o r a t o r模式仅从外部改变组件,因此组件无需对它的装饰有任何了解;也就是说,
这些装饰对该组件是透明的,如下图所示。
在S t r a t e g y模式中, c o m p o n e n t组件本身知道可能进行哪些扩充,因此它必须引用并维护
相应的策略,如下图所示。
基于S t r a t e g y的方法可能需要修改c o m p o n e n t组件以适应新的扩充。另一方面,一个策略
可以有自己特定的接口,而装饰的接口则必须与组件的接口一致。例如,一个绘制边框的策
略仅需要定义生成边框的接口( D r a w B o r d e r, GetWi d t h等),这意味着即使C o m p o n e n t类很庞
大时,策略也可以很小。
1 1 8 设计模式:可复用面向对象软件的基础
decorator-扩展的功能
strategy-扩展的功能
M a c A p p和B e d r o c k中,这种方法不仅仅用于装饰视图,还用于增强对象的事件处理能力。
在这两个系统中,每个视图维护一个“行为”对象列表,这些对象可以修改和截获事件。在
已注册的行为对象被没有注册的行为有效的重定义之前,这个视图给每个已注册的对象一个
处理事件的机会。可以用特殊的键盘处理支持装饰一个视图,例如,可以注册一个行为对象
截取并处理键盘事件。
10. 代码示例
以下C + +代码说明了如何实现用户接口装饰。我们假定已经存在一个C o m p o n e n t类
Vi s u a l C o m p o n e n t。
我们定义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装饰由_ c o m p o n e n t实例变量引用的Vi s u a l C o m p o n e n t,这个实例变量在构造器中
被初始化。对于Vi s u a l C o m p o n e n t接口中定义的每一个操作, D e c o r a t o r类都定义了一个缺省的
实现,这一实现将请求转发给_ c o m p o n e n t:
D e c o r a t o r的子类定义了特殊的装饰功能,例如, B o r d e r D e c o r a t o r类为它所包含的组件添
加了一个边框。B o r d e r D e c o r a t o r是D e c o r a t o r的子类,它重定义D r a w操作用于绘制边框。同时
B o r d e r D e c o r a t o r还定义了一个私有的辅助操作D r a w B o r d e r,由它绘制边框。这些子类继承了
D e c o r a t o r类所有其他的操作。
第4章结构型模式1 1 9
类似的可以实现S c r o l l D e c o r a t o r和D r o p S h a d o w D e c o r a t o r,它们给可视组件添加滚动和阴
影功能。
现在我们组合这些类的实例以提供不同的装饰效果,以下代码展示了如何使用D e c o r a t o r
创建一个具有边界的可滚动Te x t Vi e w.
首先我们要将一个可视组件放入窗口对象中。我们假设Wi n d o w类为此已经提供了一个
S e t C o n t e n t s操作:
现在我们可以创建一个正文视图以及放入这个正文视图的窗口:
Te x t Vi e w是一个Vi s u a l C o m p o n e n t,它可以放入窗口中:
但我们想要一个有边界的和可以滚动的Te x t Vi e w,因此我们在将它放入窗口之前对其进
行装饰:
由于Wi n d o w通过Vi s u a l C o m p o n e n t接口访问它的内容,因此它并不知道存在该装饰。如
果你需要直接与正文视图交互,例如,你想调用一些操作,而这些操作不是Vi s u a l C o m p o n e n t
接口的一部分,此时你可以跟踪正文视图。依赖于组件标识的客户也应该直接引用它。
11. 已知应用
许多面向对象的用户界面工具箱使用装饰为窗口组件添加图形装饰,例如I n t e r Vi e w s
[ LVC89, LCI+92], ET++[WGM88]和O b j e c t Wo r k s \ S m a l l t a l k类库[ P a r 9 0 ]。一些D e c o r a t o r模式的
比较特殊的应用有I n t e r Vi e w s的D e b u g g i n g G l y p h和ParcPlace Smalltalk的P a s s i v i t y Wr a p p e r。
D e b u g g i n g G l y p h在向它的组件转发布局请求前后,打印出调试信息。这些跟踪信息可用于分
析和调试一个复杂组合中对象的布局行为。P a s s i v i t y Wr a p p e r可以允许和禁止用户与组件的交
互。
但是D e c o r a t o r模式不仅仅局限于图形用户界面,下面的例子(基于E T + +的s t r e a m i n g类
[ W G M 8 8 ])说明了这一点。
S t r e a m s是大多数I / O设备的基础抽象结构,它提供了将对象转换成为字节或字符流的操作
接口,使我们可以将一个对象转变成一个文件或内存中的字符串,可以在以后恢复使用。一
个简单直接的方法是定义一个抽象的S t r e a m类,它有两个子类M e m o r y S t r e a m与F i l e S t r e a m。
但假定我们还希望能够做下面一些事情:
1 2 0 设计模式:可复用面向对象软件的基础
• 用不同的压缩算法(行程编码, Lempel-Ziv等)对数据流进行压缩。
• 将流数据简化为7位A S C I I码字符,这样它就可以在A S C I I信道上传输。
D e c o r a t o r模式提供的将这些功能添加到S t r e a m中方法很巧妙。下面的类图给出了一个解
决问题的方法。
S t r e a m抽象类维持了一个内部缓冲区并提供一些操作( PutInt, PutString)用于将数据存
入流中。一旦这个缓冲区满了, S t r e a m就会调用抽象操作H a n d l e B u ff e r F u l l进行实际数据传输。
在F i l e S t r e a m中重定义了这个操作,将缓冲区中的数据传输到文件中去。
这里的关键类是S t r e a m D e c o r a t o r,它维持了一个指向组件流的指针并将请求转发给它,
S t r e a m D e c o r a t o r子类重定义H a n d l e B u ff e r F u l l 操作并且在调用S t r e a m D e c o r a t o r的
H a n d l e B u ff e r F u l l操作之前执行一些额外的动作。
例如,C o m p r e s s i n g S t r e a m子类用于压缩数据,而A S C I I 7 S t r e a m将数据转换成7位A S C I I码。
现在我们创建F i l e S t r e a m类,它首先将数据压缩,然后将压缩了的二进制数据转换成为7位
A S C I I码,我们用C o m p r e s s i n g S t r e a m和A S C I I 7 S t r e a m装饰F i l e S t r e a m:
12. 相关模式
A d a p t e r ( 4 . 1 )模式:D e c o r a t o r模式不同于A d a p t e r模式,因为装饰仅改变对象的职责而不改
变它的接口;而适配器将给对象一个全新的接口。
C o m p o s i t e ( 4 . 3 )模式:可以将装饰视为一个退化的、仅有一个组件的组合。然而,装饰仅
给对象添加一些额外的职责—它的目的不在于对象聚集。
S t r a t e g y ( 5 . 9 )模式:用一个装饰你可以改变对象的外表;而S t r a t e g y模式使得你可以改变
对象的内核。这是改变对象的两种途径。
4.5 FACADE(外观)—对象结构型模式
1. 意图
为子系统中的一组接口提供一个一致的界面, F a c a d e模式定义了一个高层接口,这个接
第4章结构型模式1 2 1
口使得这一子系统更加容易使用。
2. 动机
将一个系统划分成为若干个子系统有利于降低系统的复杂性。一个常见的设计目标是使
子系统间的通信和相互依赖关系达到最小。达到该目标的途径之一是就是引入一个外观
(f a c a d e)对象,它为子系统中较一般的设施提供了一个单一而简单的界面。
例如有一个编程环境,它允许应用程序访问它的编译子系统。这个编译子系统包含了若
干个类,如S c a n n e r、P a r s e r、P r o g r a m N o d e、B y t e c o d e S t r e a m和P r o g r a m N o d e B u i l d e r,用于实
现这一编译器。有些特殊应用程序需要直接访问这些类,但是大多数编译器的用户并不关心
语法分析和代码生成这样的细节;他们只是希望编译一些代码。对这些用户,编译子系统中
那些功能强大但层次较低的接口只会使他们的任务复杂化。
为了提供一个高层的接口并且对客户屏蔽这些类,编译子系统还包括一个C o m p l i e r类。
这个类定义了一个编译器功能的统一接口。C o m p i l e r类是一个外观,它给用户提供了一个单
一而简单的编译子系统接口。它无需完全隐藏实现编译功能的那些类,即可将它们结合在一
起。编译器的外观可方便大多数程序员使用,同时对少数懂得如何使用底层功能的人,它并
不隐藏这些功能,如下图所示。
1 2 2 设计模式:可复用面向对象软件的基础
客户类
子系统类
编译子系统的类
3. 适用性
在遇到以下情况使用F a c a d e模式
• 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越
复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容
易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。
F a c a d e可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需
要更多的可定制性的用户可以越过f a c a d e层。
• 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入f a c a d e将这个子系统与客
户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
• 当你需要构建一个层次结构的子系统时,使用f a c a d e模式定义子系统中每层的入口点。
如果子系统之间是相互依赖的,你可以让它们仅通过f a c a d e进行通讯,从而简化了它们
之间的依赖关系。
4. 结构
5. 参与者
• F a c a d e ( C o m p i l e r )
— 知道哪些子系统类负责处理请求。
— 将客户的请求代理给适当的子系统对象。
• Subsystem classes ( S c a n n e r、P a r s e r、P r o g r a m N o d e等)
— 实现子系统的功能。
— 处理由F a c a d e对象指派的任务。
— 没有f a c a d e的任何相关信息;即没有指向f a c a d e的指针。
6. 协作
• 客户程序通过发送请求给F a c a d e的方式与子系统通讯, F a c a d e将这些消息转发给适当的
子系统对象。尽管是子系统中的有关对象在做实际工作,但F a c a d e模式本身也必须将它
的接口转换成子系统的接口。
• 使用F a c a d e的客户程序不需要直接访问子系统对象。
7. 效果
F a c a d e模式有下面一些优点:
1) 它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来
更加方便。
第4章结构型模式1 2 3
子系统类
2) 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。
松耦合关系使得子系统的组件变化不会影响到它的客户。F a c a d e模式有助于建立层次结构系
统,也有助于对对象之间的依赖关系分层。F a c a d e模式可以消除复杂的循环依赖关系。这一
点在客户程序与子系统是分别实现的时候尤为重要。
在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译
工作以节省时间。用F a c a d e可以降低编译依赖性,限制重要系统中较小的变化所需的重编译
工作。F a c a d e模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统
一般不需要编译所有其他的子系统。
3) 如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性和通用性之
间加以选择。
8. 实现
使用F a c a d e模式时需要注意以下几点:
1) 降低客户-子系统之间的耦合度用抽象类实现F a c a d e而它的具体子类对应于不同的子
系统实现,这可以进一步降低客户与子系统的耦合度。这样,客户就可以通过抽象的F a c a d e
类接口与子系统通讯。这种抽象耦合关系使得客户不知道它使用的是子系统的哪一个实现。
除生成子类的方法以外,另一种方法是用不同的子系统对象配置F a c a d e对象。为定制
f a c a d e,仅需对它的子系统对象(一个或多个)进行替换即可。
2) 公共子系统类与私有子系统类一个子系统与一个类的相似之处是,它们都有接口并
且它们都封装了一些东西—类封装了状态和操作,而子系统封装了一些类。考虑一个类的
公共和私有接口是有益的,我们也可以考虑子系统的公共和私有接口。
子系统的公共接口包含所有的客户程序可以访问的类;私有接口仅用于对子系统进行扩
充。当然, F a c a d e类是公共接口的一部分,但它不是唯一的部分,子系统的其他部分通常也
是公共的。例如,编译子系统中的P a r s e r类和S c a n n e r类就是公共接口的一部分。
私有化子系统类确实有用,但是很少有面向对象的编程语言支持这一点。C + +和S m a l l t a l k
语言仅在传统意义下为类提供了一个全局名空间。然而,最近C + +标准化委员会在C + +语言中
增加了一些名字空间[ S t r 9 4 ],这些名字空间使得你可以仅暴露公共子系统类。
9. 代码示例
让我们仔细观察一下如何在一个编译子系统中使用F a c a d e。
编译子系统定义了一个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 y t e c o d e对象封装一个字节码,这个字节码可用于指定机器指令。该子系统中还定义了一个
To k e n类,它封装了编程语言中的标识符。
S c a n n e r类接收字符流并产生一个标识符流,一次产生一个标识符( t o k e n )。
用P r o g r a m N o d e B u i l d e r,P a r s e r类由S c a n n e r生成的标识符构建一棵语法分析树。
1 2 4 设计模式:可复用面向对象软件的基础
P a r s e r回调P r o g r a m N o d e B u i l d e r逐步建立语法分析树,这些类遵循B u i l d e r ( 3 . 2 )模式进行交
互操作。
语法分析树由P r o g r a m N o d e子类(例如S t a t e m e n t N o d e和E x p r e s s i o n N o d e等)的实例构成。
P r o g r a m N o d e层次结构是C o m p o s i t e模式的一个应用实例。P r o g r a m N o d e定义了一个接口用于
操作程序节点和它的子节点(如果有的话)。
Tr a v e r s e操作以一个C o d e G e n e r a t o r对象为参数,P r o g r a m N o d e子类使用这个对象产生机器
代码,机器代码格式为B y t e c o d e S t r e a m中的B y t e C o d e对象。其中的C o d e G e n e r a t o r类是一个访
第4章结构型模式1 2 5
问者(参见Vi s i t o r ( 5 . 11 )模式)。
例如C o d e G e n e r a t o r类有两个子类S t a c k M a c h i n e C o d e G e n e r a t o r和R I S C C o d e G e n e r a t o r,分
别为不同的硬件体系结构生成机器代码。
P r o g r a m N o d e的每个子类在实现Tr a v e r s e时,对它的P r o g r a m N o d e子对象调用Tr a v e r s e。每
个子类依次对它的子节点做同样的动作,这样一直递归下去。例如, E x p r e s s i o n N o d e像这样
定义Tr a v e r s e:
我们上述讨论的类构成了编译子系统,现在我们引入C o m p i l e r类, C o m p l i e r类是一个
f a c a d e,它将所有部件集成在一起。C o m p i l e r提供了一个简单的接口用于为特定的机器编译源
代码并生成可执行代码。
上面的实现在代码中固定了要使用的代码生成器的种类,因此程序员不需要指定目标机
的结构。在仅有一种目标机的情况下,这是合理的。如果有多种目标机,我们可能希望改变
C o m p i l e r构造函数使之能接受C o d e G e n e r a t o r为参数,这样程序员可以在实例化C o m p i l e r时指
1 2 6 设计模式:可复用面向对象软件的基础
定要使用的生成器。编译器的f a c a d e还可以对S c a n n e r和P r o g r a m N o d e B u i l d e r这样的其他一些
参与者进行参数化以增加系统的灵活性,但是这并非F a c a d e模式的主要任务,它的主要任务
是为一般情况简化接口。
10. 已知应用
在代码示例一节中的编译器例子受到了O b j e c t Wo r k s \ S m a l l t a l k编译系统[ P a r 9 0 ]的启发。
在E T + +应用框架[ W G M 8 8 ]中,应用程序可以有一个内置的浏览工具,用于在运行时刻监
视它的对象。这些浏览工具在一个独立的子系统中实现,这一子系统包含一个称为P r o g r a m -
m i n g E n v i r o n m e n t的F a c a d e类。这个f a c a d e定义了一些操作(如I n s p e c t O b j e c t和I n s p e c t C l a s s等)
用于访问这些浏览器。
E T + +应用程序也可以不理会这些内置的浏览功能,这时P r o g r a m m i n g E n v i r o n m e n t对这些
请求用空操作实现;也就是说,它们什么也不做。仅有E T P r o g r a m m i n g E n v i r o n m e n t子类用一
些显示相应浏览器的操作实现这些请求。因此应用程序并不知道是否有内置浏览器存在,应
用程序与浏览子系统的之间仅存在抽象的耦合关系。
C h o i c e s操作系统[ C I R M 9 3 ]使用f a c a d e模式将多个框架组合到一起。C h o i c e s中的关键抽象
是进程( p r o c e s s )、存储( s t o r a g e )和地址空间(address space)。每个抽象有一个相应的子系统,
用框架实现,支持C h o i c e s系统在不同的硬件平台之间移植。其中的两个子系统有“代表”
(也就是f a c a d e),这两个代表分别是存储( F i l e S y s t e m I n t e r f a c e )和地址空间( D o m a i n )。
例如,虚拟存储框架将D o m a i n作为其f a c a d e。一个D o m a i n代表一个地址空间。它提供了
虚存地址到内存对象、文件系统或后备存储设备( backing store)的偏移量之间的一个映射。
D o m a i n支持在一个特定地址增加内存对象、删除内存对象以及处理页面错误。
正如上图所示,虚拟存储子系统内部有以下一些组件:
• MemoryObject表示数据存储。
• M e m o r y O b j e c t C a c h e将M e m o r y O b j e c t数据缓存在物理存储器中。M e m o r y O b j e c t C a c h e实
际上是一个S t r a t e g y ( 5 . 9 )模式,由它定位缓存策略。
• AddressTr a n s l a t i o n封装了地址翻译硬件。
第4章结构型模式1 2 7
虚拟内存框
当发生缺页中断时,调用R e p a i r F a u l t操作,D o m a i n在引起缺页中断的地址处找到内存对
象并将R e p a i r F a u l t操作代理给与这个内存对象相关的缓存。可以改变D o m a i n的组件对D o m a i n
进行定制。
11. 相关模式
Abstract Factory (3.1)模式可以与F a c a d e模式一起使用以提供一个接口,这一接口可用来
以一种子系统独立的方式创建子系统对象。Abstract Factory也可以代替F a c a d e模式隐藏那些
与平台相关的类。
Mediator (5.5) 模式与F a c a d e模式的相似之处是,它抽象了一些已有的类的功能。然而,
M e d i a t o r的目的是对同事之间的任意通讯进行抽象,通常集中不属于任何单个对象的功能。
M e d i a t o r的同事对象知道中介者并与它通信,而不是直接与其他同类对象通信。相对而言,
F a c a d e模式仅对子系统对象的接口进行抽象,从而使它们更容易使用;它并不定义新功能,
子系统也不知道f a c a d e的存在。
通常来讲,仅需要一个F a c a d e对象,因此F a c a d e对象通常属于Singleton (3.5)模式。
4.6 FLYWEIGHT(享元)—对象结构型模式
1. 意图
运用共享技术有效地支持大量细粒度的对象。
2. 动机
有些应用程序得益于在其整个设计过程中采用对象技术,但简单化的实现代价极大。
例如,大多数文档编辑器的实现都有文本格式化和编辑功能,这些功能在一定程度上是
模块化的。面向对象的文档编辑器通常使用对象来表示嵌入的成分,例如表格和图形。尽管
用对象来表示文档中的每个字符会极大地提高应用程序的灵活性,但是这些编辑器通常并不
这样做。字符和嵌入成分可以在绘制和格式化时统一处理,从而在不影响其他功能的情况下
能对应用程序进行扩展,支持新的字符集。应用程序的对象结构可以模拟文档的物理结构。
下图显示了一个文档编辑器怎样使用对象来表示字符。
但这种设计的缺点在于代价太大。即使是一个中等大小的文档也可能要求成百上千的字
符对象,这会耗费大量内存,产生难以接受的运行开销。所以通常并不是对每个字符都用一
1 2 8 设计模式:可复用面向对象软件的基础
字符对象
行对象
列对象
个对象来表示的。F l y w e i g h t模式描述了如何共享对象,使得可以细粒度地使用它们而无需高
昂的代价。
f l y w e i g h t是一个共享对象,它可以同时在多个场景( c o n t e x 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 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。
Fl 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 l y w e i g h t对象的共享池中。
这些对象的类结构如下图所示。G l y p h是图形对象的抽象类,其中有些对象可能是
f l y w e i g h t。基于外部状态的那些操作将外部状态作为参量传递给它们。例如, D r a w和
I n t e r s e c t s在执行之前,必须知道g l y p h所在的场景,如下页上图所示。
表示字母“a”的f l y w e i g h t只存储相应的字符代码;它不需要存储字符的位置或字体。用
户提供与场景相关的信息,根据此信息f l y w e i g h t绘出它自己。例如, Row glyph知道它的子女
应该在哪儿绘制自己才能保证它们是横向排列的。因此Row glyph可以在绘制请求中向每一个
子女传递它的位置。
第4章结构型模式1 2 9
由于不同的字符对象数远小于文档中的字符数,因此,对象的总数远小于一个初次执行
的程序所使用的对象数目。对于一个所有字符都使用同样的字体和颜色的文档而言,不管这
个文档有多长,需要分配1 0 0个左右的字符对象(大约是A S C I I字符集的数目)。由于大多数文
档使用的字体颜色组合不超过1 0种,实际应用中这一数目不会明显增加。因此,对单个字符
进行对象抽象是具有实际意义的。
3. 适用性
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对象可以被共享,对于概念上明显有别的对
象,标识测试将返回真值。
4. 结构
1 3 0 设计模式:可复用面向对象软件的基础
下面的对象图说明了如何共享f l y w e i g h t。
5. 参与者
• F l y w e i g h t ( G l y p h )
— 描述一个接口,通过这个接口f l y w e i g h t可以接受并作用于外部状态。
• C o n c r e t e F l y w e i g h t( C h a r a c t e r )
— 实现F l y w e i g h t 接口, 并为内部状态( 如果有的话) 增加存储空间。
C o n c r e t e F l y w e i g h t对象必须是可共享的。它所存储的状态必须是内部的;即,它必
须独立于C o n c r e t e F l y w e i g h t对象的场景。
• U n s h a r e d C o n c r e t e F l y w e i g h t ( R o w, C o l u m n )
— 并非所有的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对象结构的某些层次, U n s h a r e d C o n c r e t e F l y w e i g h t对象通常
将C o n c r e t e F l y w e i g h t对象作为子节点(R o w和C o l u m n就是这样)。
• F l y w e i g h t F a c t o r y
— 创建并管理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 t o r y对象提供
一个已创建的实例或者创建一个(如果不存在的话)。
• Client
— 维持一个对f l y w e i g h t的引用。
— 计算或存储一个(多个) f l y w e i g h t的外部状态。
6. 协作
• f l y w e i g h t执行时所需的状态必定是内部的或外部的。内部状态存储于C o n c r e t e F l y w e i g h t
对象之中;而外部对象则由C l i e n t对象存储或计算。当用户调用f l y w e i g h t对象的操作时,
将该状态传递给它。
• 用户不应直接对C o n c r e t e F l y w e i g h t类进行实例化,而只能从F l y w e i g h t F a c t o r y对象得到
C o n c r e t e F l y w e i g h t对象,这可以保证对它们适当地进行共享。
7. 效果
使用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
越多,空间节省也就越大。
存储节约由以下几个因素决定:
第4章结构型模式1 3 1
• 因为共享,实例总数减少的数目
• 对象内部状态的平均数目
• 外部状态是计算的还是存储的
共享的F l y w e i g h t越多,存储节约也就越多。节约量随着共享状态的增多而增大。当对象
使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最
大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外
部状态的存储。
F l y w e i g h t模式经常和C o m p o s i t e(4 . 3)模式结合起来表示一个层次式结构,这一层次式
结构是一个共享叶节点的图。共享的结果是, F l y w e i g h t的叶节点不能存储指向父节点的指针。
而父节点的指针将传给F l y w e i g h t作为它的外部状态的一部分。这对于该层次结构中对象之间
相互通讯的方式将产生很大的影响。
8. 实现
在实现F l y w e i g h t模式时,注意以下几点:
1 ) 删除外部状态该模式的可用性在很大程度上取决于是否容易识别外部状态并将它从
共享对象中删除。如果不同种类的外部状态和共享前对象的数目相同的话,删除外部状态不
会降低存储消耗。理想的状况是,外部状态可以由一个单独的对象结构计算得到,且该结构
的存储要求非常小。
例如,在我们的文档编辑器中,我们可以用一个单独的结构存储排版布局信息,而不是
存储每一个字符对象的字体和类型信息,布局图保持了带有相同排版信息的字符的运行轨迹。
当某字符绘制自己的时候,作为绘图遍历的副作用它接收排版信息。因为通常文档使用的字
体和类型数量有限,将该信息作为外部信息来存储,要比内部存储高效得多。
2 ) 管理共享对象因为对象是共享的,用户不能直接对它进行实例化,因此F l y w e i g h t -
F a c t o r y可以帮助用户查找某个特定的F l y w e i g h t对象。F l y w e i g h t F a c t o r y对象经常使用关联存储
帮助用户查找感兴趣的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 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的数目固定而且很小的时候(例如,用于A C S I I码的
F l y w e i g h t),这两种操作都不必要。在这种情况下, F l y w e i g h t完全可以永久保存。
9. 代码示例
回到我们文档编辑器的例子,我们可以为F l y w e i g h t的图形对象定义一个G l y p h基类。逻辑
上, G l y p h是一些C o m p o s i t e类(见C o m p o s i t e(4 . 3)),它有图形化属性,并可以绘制自己。
这里,我们重点讨论字体属性,但这种方法也同样适用于Gl y p h的其他图形属性。
1 3 2 设计模式:可复用面向对象软件的基础
Character 的子类存储一个字符代码:
为了避免给每一个G l y p h的字体属性都分配存储空间,我们可以将该属性外部存储于
G l y p h C o n t e x t对象中。G l y p h C o n t e x t是一个外部状态的存储库,它维持Gl y p h与字体(以及其
他一些可能的图形属性)之间的一种简单映射关系。对于任何操作,如果它需要知道在给定
场景下G l y p h字体,都会有一个G l y p h C o n t e x t实例作为参数传递给它。然后,该操作就可以查
询G l y p h C o n t e x t以获取该场景中的字体信息了。这个场景取决于G l y p h结构中的G l y p h的位置。
因此,当使用G l y p h时,G l y p h子类的迭代和管理操作必须更新G l y p h C o n t e x t。
在遍历过程中, G l y p h C o n t e x t必须它在Gl y p h结构中的当前位置。随着遍历的进行,
G l y p h C o n t e x t : : N e x t增加_ i n d e x的值。G l y p h的子类(如, R o w和C o l u m n)对N e x t操作的实现
必须使得它在遍历的每一点都调用G l y p h C o n t e x t : : N e x t。
G l y p h C o n t e x t : : G e t F o c u s将索引作为B t r e e结构的关键字, B t r e e结构存储g l y p h到字体的映
射。树中的每个节点都标有字符串的长度,而它给这个字符串字体信息。树中的叶节点指向
一种字体,而内部的字符串分成了很多子字符串,每一个对应一种子节点。
下页上图是从一个g l y p h组合中截取出来的:
字体信息的B Tr e e结构可能如下:
内部节点定义Gl y p h索引的范围。当字体改变或者在Gl y p h结构中添加或删除Gl y p h时,
B t r e e将相应地被更新。例如,假定我们遍历到索引1 0 2,以下代码将单词“e x c e p t”的每个字符
第4章结构型模式1 3 3
的字体设置为它周围的正文的字体(即,用Time 12字体,12-point Times Roman的一个实例):
新的B t r e e结构如下图(黑体显示变化):
1 3 4 设计模式:可复用面向对象软件的基础
假设我们要在单词“ expect ”前用12-point Times Italic字体添加一个单词D o n’t(包括一
个紧跟着的空格)。假定g c仍在索引位置1 0 2,以下代码通知g c这个事件:
B t r e e结构变为如下图所示:
当向G l y p h C o n t e x t查询当前Gl y p h的字体时,它将向下搜寻B t r e e,同时增加索引,直至找
到当前索引的字体为止。由于字体变化频率相对较低,所以这棵树相对于Gl y p h结构较小。这
将使得存储耗费较小,同时也不会过多的增加查询时间。
F l y w e i g h t F a c t o r y是我们需要的最后一个对象,它负责创建G l y p h并确保对它们进行合理
共享。G l y p h F a c t o r y类将实例化C h a r a c t e r和其他类型的G l y p h。我们只共享C h a r a c t e r对象;组
合的G l y p h要少得多,并且它们的重要状态(如,他们的子节点)必定是内部的。
_ c h a r a c t e r数组包含一些指针,指向以字母代码为索引的Character Glyphs。该数组在构造
函数中被初始化为零。
C r e a t e C h a r a c t e r在字母符号数组中查找一个字符,如果存在的话,返回相应的G l y p h。若
不存在,C r e a t e C h a r a c t e r就创建一个G l y p h,将其放入数组中,并返回它:
第4章结构型模式1 3 5
本机制中的查询时间与字体的变化频率成比例。当每一个字符的字体均不同时,性能最差,但通常这种情
况极少。
其他操作仅需在每次被调用时实例化一个新对象,因为非字符的G l y p h不能被共享:
我们可以忽略这些操作,让用户直接实例化非共享的G l y p h。然而,如果我们想让这些符
号以后可以被共享,必须改变创建它们的客户程序代码。
10. 已知应用
F l y w e i g h t的概念最先是在I n t e r View 3.0[CL90]中提出并作为一种设计技术得到研究。它
的开发者构建了一个强大的文档编辑器D o c ,作为f l y w e i g h t概念的论证[ C L 9 2 ]。D o c使用符号对
象来表示文档中的每一个字符。编辑器为每一个特定类型(定义它的图形属性)的字符创建一个
G l y p h实例;所以,一个字符的内部状态包括字符代码和类型信息(类型表的索引)。这意味
着只有位置是外部状态,这就使得D o c运行很快。文档由类D o c u m e n t表示,它同时也是一个
F l y w e i g h t F a c t o r y。对D o c的测试表明共享F l y w e i g h t字符是高效的。通常,一个包含1 8 0 0 0 0个
字符的文档只要求分配大约4 8 0个字符对象。
ET++ [WGM88]使用F l y w e i g h t来支持视觉风格独立性。视觉风格标准影响用户界面各
部分的布局(如,滚动条、按钮、菜单-统称为“窗口组件”)和它们的修饰成分(如,阴影、
斜角)。w i d g e t将所有布局和绘制行为代理给一个单独的L a y o u t对象。改变L a y o u t对象会改变
视觉风格,即使在运行时刻也是这样。
每一个w i d g e t类都有一个L a y o u t类与之相对应(如S c o l l b a r L a y o u t、M e n u b a r L a y o u t等)。
使用这种方法,一个明显的问题是,使用单独的L a y o u t对象会使用户界面对象成倍增加,因
为对每个用户界面对象,都会有一个附加的L a y o u t对象。为了避免这种开销,可用F l y w e i g h t
实现L a y o u t对象。用F l y w e i g h t的效果很好,因为它们主要处理行为定义,而且很容易将一些
较小的外部状态传递给它们,它们需要用这些状态来安排一个对象的位置或者对它进行绘制。
对象L a y o u t由L o o k对象创建和管理。L o o k类是一个Abstract Factory (3 . 1 ),它用
G e t B u t t o n L a y o u t和G e t M e n u B a r L a y o u t这样的操作检索一个特定的L a y o u t对象。对于每一个
视觉风格标准,都有一个相应的L o o k子类(如M o t i f L o o k、O p e n L o o k)提供相应的L a y o u t对
象。
顺便提一下,L a y o u t对象其实是S t r a t e g y(参见S t r a t e g y ( 5 . 9 )模式)。他们是用F l y w e i g h t实
现的S t r a t e g y对象的一个例子。
11. 相关模式
1 3 6 设计模式:可复用面向对象软件的基础
在前面的代码示例一节中,类型信息是外部的,所以只有字符代码是内部状态。
实现视觉风格独立的另一种方法可参见Abstract Factory(3.1)模式。
F l y w e i g h t模式通常和C o m p o s i t e ( 4 . 3 )模式结合起来,用共享叶结点的有向无环图实现一个
逻辑上的层次结构。
通常,最好用F l y w e i g h t实现S t a t e ( 5 . 8 )和S t r a t e g y ( 5 . 9 )对象。
4.7 PROXY(代理)—对象结构型模式
1. 意图
为其他对象提供一种代理以控制对这个对象的访问。
2. 别名
Surrogate
3. 动机
对一个对象进行访问控制的一个原因是为了只有在我们确实需要这个对象时才对它进行
创建和初始化。我们考虑一个可以在文档中嵌入图形对象的文档编辑器。有些图形对象(如
大型光栅图像)的创建开销很大。但是打开文档必须很迅速,因此我们在打开文档时应避免
一次性创建所有开销很大的对象。因为并非所有这些对象在文档中都同时可见,所以也没有
必要同时创建这些对象。
这一限制条件意味着,对于每一个开销很大的对象,应该根据需要进行创建,当一个图
像变为可见时会产生这样的需要。但是在文档中我们用什么来代替这个图像呢?我们又如何
才能隐藏根据需要创建图像这一事实,从而不会使得编辑器的实现复杂化呢?例如,这种优
化不应影响绘制和格式化的代码。
问题的解决方案是使用另一个对象,即图像P r o x y,替代那个真正的图像。P r o x y可以代
替一个图像对象,并且在需要时负责实例化这个图像对象。
只有当文档编辑器激活图像代理的D r a w操作以显示这个图像的时候,图像P r o x y才创建真
正的图像。P r o x y直接将随后的请求转发给这个图像对象。因此在创建这个图像以后,它必须
有一个指向这个图像的引用。
我们假设图像存储在一个独立的文件中。这样我们可以把文件名作为对实际对象的引用。
P r o x y还存储了图像的尺寸(e x t e n t),即它的长和宽。有了图像尺寸, P r o x y无须真正实例化
这个图像就可以响应格式化程序对图像尺寸的请求。
以下的类图更详细地阐述了这个例子。
文档编辑器通过抽象的G r a p h i c类定义的接口访问嵌入的图像。I m a g e P r o x y是那些根据需
要创建的图像的类, I m a g e P r o x y保存了文件名作为指向磁盘上的图像文件的指针。该文件名
被作为一个参数传递给I m a g e P r o x y的构造器。
I m a g e P r o x y还存储了这个图像的边框以及对真正的I m a g e实例的指引,直到代理实例化真
正的图像时,这个指引才有效。D r a w操作必须保证在向这个图像转发请求之前,它已经被实
例化了。G e t E x t e n t操作只有在图像被实例化后才向它传递请求,否则, I m a g e P r o x y返回它存
第4章结构型模式1 3 7
内存中硬盘上
储的图像尺寸。
4. 适用性
在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用P r o x y模式。下面是一
些可以使用P r o x y模式常见情况:
1) 远程代理( Remote Proxy ) 为一个对象在不同的地址空间提供局部代表。
NEXTSTEP[Add94] 使用N X P r o x y类实现了这一目的。Coplien[Cop92] 称这种代理为“大使”
(A m b a s s a d o r)。
2 ) 虚代理(Virtual Proxy)根据需要创建开销很大的对象。在动机一节描述的I m a g e P r o x y
就是这样一种代理的例子。
3) 保护代理(Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同
的访问权限的时候。例如,在C h o i c e s操作系统[ C I R M 9 3 ]中K e m e l P r o x i e s为操作系统对象提供
了访问保护。
4 ) 智能指引(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。
它的典型用途包括:
• 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为S m a r t
P o i n t e r s[ E d e 9 2 ] )。
• 当第一次引用一个持久对象时,将它装入内存。
• 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
5. 结构
1 3 8 设计模式:可复用面向对象软件的基础
这是运行时刻一种可能的代理结构的对象图。
6. 参与者
• P r o x y ( I m a g e P r o x y )
— 保存一个引用使得代理可以访问实体。若R e a l S u b j e c t和S u b j e c t的接口相同,P r o x y会
引用S u b j e c t。
— 提供一个与S u b j e c t的接口相同的接口,这样代理就可以用来替代实体。
— 控制对实体的存取,并可能负责创建和删除它。
— 其他功能依赖于代理的类型:
• Remote Pro x y负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编
码的请求。
• Vi rtual Pro x y可以缓存实体的附加信息,以便延迟对它的访问。例如,动机一节中提
到的I m a g e P r o x y缓存了图像实体的尺寸。
• P rotection Pro x y检查调用者是否具有实现一个请求所必需的访问权限。
• S u b j e c t ( G r a p h i c )
— 定义RealSubject 和P r o x y的共用接口,这样就在任何使用R e a l S u b j e c t的地方都可以使
用P r o x y。
• R e a l S u b j e c t ( I m a g e )
— 定义P r o x y所代表的实体。
7. 协作
• 代理根据其种类,在适当的时候向R e a l S u b j e c t转发请求。
8. 效果
P r o x y模式在访问对象时引入了一定程度的间接性。根据代理的类型,附加的间接性有多
种用途:
1 ) Remote Proxy可以隐藏一个对象存在于不同地址空间的事实。
2) Virtual Proxy 可以进行最优化,例如根据要求创建对象。
3) Protection Proxies和Smart Reference都允许在访问一个对象时有一些附加的内务处理
(Housekeeping task)。
P r o x y模式还可以对用户隐藏另一种称之为c o p y - o n - w r i t e的优化方式,该优化与根据需要
创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有
被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对
象被修改的时候才对它进行拷贝。
在实现C o p y - o n - w r i t e时必须对实体进行引用计数。拷贝代理仅会增加引用计数。只有当
用户请求一个修改该实体的操作时,代理才会真正的拷贝它。在这种情况下,代理还必须减
少实体的引用计数。当引用的数目为零时,这个实体将被删除。
C o p y - o n - Wr i t e可以大幅度的降低拷贝庞大实体时的开销。
9. 实现
第4章结构型模式1 3 9
P r o x y模式可以利用以下一些语言特性:
1 ) 重载C + +中的存取运算符C + +支持重载运算符- >。重载这一运算符使你可以在撤消对
一个对象的引用时,执行一些附加的操作。这一点可以用于实现某些种类的代理;代理的作
用就象一个指针。
下面的例子说明怎样使用这一技术实现一个称为I m a g e P t r的虚代理。
重载的- >和*运算符使用L o a d I m a g e将_ i m a g e返回给它的调用者(如果必要的话装入它)。
该方法使你能够通过I m a g e P t r对象调用I m a g e操作,而省去了把这些操作作为I m a g e P t r接
口的一部分的麻烦。
请注意这里的i m a g e代理起到一个指针的作用,但并没有将它定义为一个指向I m a g e的指
针。这意味着你不能把它当作一个真正的指向I m a g e的指针来使用。因此在使用此方法时用户
应区别对待I m a g e对象和I m a g e p t r对象。
重载成员访问运算符并非对每一种代理来说都是好办法。有些代理需要清楚地知道调用
了哪个操作,重载运算符的方法在这种情况下行不通。
考虑在目的一节提到的虚代理的例子,图像应该在一个特定的时刻被装载—也就是在
1 4 0 设计模式:可复用面向对象软件的基础
D r a w操作被调用时—而不是在只要引用这个图像就装载它。重载访问操作符不能作出这种
区分。在这种情况下我们只能人工实现每一个代理操作,向实体转发请求。
正如示例代码中所示的那样,这些操作之间非常相似。一般来说,所有的操作在向实体
转发请求之前,都要检验这个要求是否合法,原始对象是否存在等。但重复写这些代码很麻
烦,因此我们一般用一个预处理程序自动生成它。
2) 使用S m a l l t a l k中的d o e s N o t U n d e r s t a n d S m a l l t a l k提供一个h o o k方法可以用来自动转发
请求。当用户向接受者发送一个消息,但是这个接受者没有相关方法的时候, S a m l l t a l k调用
方法doesNotUnderstand: amessage。P r o x y类可以重定义d o e s N o t U n d e r s t a n d以便向它的实体转
发这个消息。
为了保证一个请求真正被转发给实体,而不是无声无息的被代理所吸收,我们可以定义
一个不理解任何信息的P r o x y类。S m a l l t a l k定义了一个没有任何超类的P r o x y类,实现了这个
目的。
d o e s N o t U n d e r s t a n d:的主要缺点在于:大多数S m a l l t a l k系统都有一些由虚拟机直接控制
的特殊消息,而这些消息并不引起通常的方法查找。唯一一个通常用O b j e c t实现(因而可以影
响代理)的符号是恒等运算符= =。
如果你准备使用d o e s N o t U n d e r s t a n d:来实现P r o x y的话,你必须围绕这一问题进行设计。
对代理的标识并不意味着对真正实体的标识。d o e s N o t U n d e r s t a n d:另一个缺点是,它主要用
作错误处理,而不是创建代理,因此一般来说它的速度不是很快。
3) Proxy并不总是需要知道实体的类型若P r o x y类能够完全通过一个抽象接口处理它的
实体,则无须为每一个R e a l S u b j e c t类都生成一个P r o x y类; P r o x y可以统一处理所有的
R e a l S u b j e c t类。但是如果P r o x y要实例化RealSubjects (例如在virtual proxy中),那么它们必须
知道具体的类。
另一个实现方面的问题涉及到在实例化实体以前怎样引用它。有些代理必须引用它们的
实体,无论它是在硬盘上还是在内存中。这意味着它们必须使用某种独立于地址空间的对象
标识符。在目的一节中,我们采用一个文件名来实现这种对象标识符。
10. 代码示例
以下代码实现了两种代理: 在目的一节描述的Virtual Proxy,和用d o e s N o t U n d e r s t a n d :实现
的P r o x y。
1 ) Virtual Proxy Graphic类为图形对象定义一个接口。
第4章结构型模式1 4 1
对N E X T S T E P [ A d d 9 4 ]中的分布式对象(尤其是类N X P r o x y )的实现就使用了该技术。N E X T S T E P中等价的
h o o k方法是f o r w a r d,这一实现重定义了f o r w a r d方法。
I t e r a t o r模式(5 . 4)描述了另一种类型的P r o x y。
I m a g e类实现了G r a p h i c接口用来显示图像文件。I m a g e重定义H a n d l e m o u s e操作,使得用
户可以交互的调整图像的尺寸。
I m a g e P r o x y和I m a g e具有相同的接口:
构造函数保存了存储图像的文件名的本地拷贝,并初始化_ e x t e n t和_ i m a g e:
如果可能的话, G e t E x t e n t的实现部分返回缓存的图像尺寸;否则从文件中装载图像。
D r a w用来装载图像,H a n d e l M o u s e则向实际图像转发这个事件。
1 4 2 设计模式:可复用面向对象软件的基础
S a v e操作将缓存的图像尺寸和文件名保存在一个流中。L o a d得到这个信息并初始化相应
的成员函数。
最后,假设我们有一个类Te x t D o c u m e n t能够包含G r a p h i c对象:
我们可以用以下方式把I m a g e P r o x y插入到文本文件中。
2 ) 使用d o e s N o t U n d e r s t a n d的Proxy 在S m a l l t a l k中,你可以定义超类为n i l 的类,同时定
义doesNotUnderstand: 方法处理消息,这样构建一些通用的代理。
在以下程序中我们假设代理有一个r e a l S u b j e c t方法,该方法返回它的实体。在I m a g e P r o x y
中,该方法将检查是否已创建了I m a g e,并在必要的时候创建它,最后返回I m a g e。它使用
perform: withArg u m e n t s :来执行被保留在实体中的那些消息。
d o e s N o t U n d e r s t a n d :的参数是M e s s a g e的一个实例,它表示代理不能理解的消息。所以,
代理在转发消息给实体之前,首先确定实体的存在性,并由此对所有的消息做出响应。
d o e s N o t U n d e r s t a n d :的一个优点是它可以执行任意的处理过程。例如,我们可以用这样的
方式生成一个protection proxy,即指定一个可以接受的消息的集合l e g a l M e s s a g e s,然后给这
个代理定义以下方法。
第4章结构型模式1 4 3
几乎所有的类最终均以O b j e c t(对象)作为他们的超类。所以说这句话等于说“定义了一个类,它的超类
不是O b j e c t”。
这个方法在向实体转发一个消息之前,检查它的合法性。如果不是合法的,那么发送
error: 给代理,除非代理定义e r r o r :,这将产生一个错误的无限循环。因此, e r r o r :的定义应该
同所有它用到的方法一起从O b j e c t类中拷贝。
11. 已知应用
动机一节中virtual proxy的例子来自于E T + +的文本构建块类。
N E X T S T E P [ A d d 9 4 ]使用代理(类N X P r o x y的实例)作为可分布对象的本地代表,当客户请
求远程对象时,服务器为这些对象创建代理。收到消息后,代理对消息和它的参数进行编码,
并将编码后的消息传递给远程实体。类似的,实体对所有的返回结果编码,并将它们返回给
N X P r o x y对象。
McCullough [McC87]讨论了在S m a l l t a l k中用代理访问远程对象的问题。Pascoe [Pas86]讨
论了如何用“封装器”(E n c a p s u l a t o r s)控制方法调用的副作用以及进行访问控制。
12. 相关模式
A d a p t e r ( 4 . 1 ):适配器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的目的不一样。
D e c o r a t o r为对象添加一个或多个功能,而代理则控制对对象的访问。
代理的实现与d e c o r a t o r的实现类似,但是在相似的程度上有所差别。Protection Proxy的
实现可能与d e c o r a t o r的实现差不多。另一方面, Remote Proxy不包含对实体的直接引用,而
只是一个间接引用,如“主机I D,主机上的局部地址。”Virtual Proxy开始的时候使用一个间
接引用,例如一个文件名,但最终将获取并使用一个直接引用。
4.8 结构型模式的讨论
你可能已经注意到了结构型模式之间的相似性,尤其是它们的参与者和协作之间的相似
性。这可能是因为结构型模式依赖于同一个很小的语言机制集合构造代码和对象:单继承和
多重继承机制用于基于类的模式,而对象组合机制用于对象式模式。但是这些相似性掩盖了
这些模式的不同意图。在本节中,我们将对比这些结构型模式,使你对它们各自的优点有所
了解。
4.8.1 Adapter与Bridge
A d a p t e r(4 . 1)模式和B r i d g e(4 . 2)模式具有一些共同的特征。它们都给另一对象提供
了一定程度上的间接性,因而有利于系统的灵活性。它们都涉及到从自身以外的一个接口向
这个对象转发请求。
这些模式的不同之处主要在于它们各自的用途。A d a p t e r模式主要是为了解决两个已有接
口之间不匹配的问题。它不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化。
这种方式不需要对两个独立设计的类中的任一个进行重新设计,就能够使它们协同工作。另
1 4 4 设计模式:可复用面向对象软件的基础
一方面, B r i d g e模式则对抽象接口与它的(可能是多个)实现部分进行桥接。虽然这一模式
允许你修改实现它的类,它仍然为用户提供了一个稳定的接口。B r i d g e模式也会在系统演化
时适应新的实现。
由于这些不同点, A d a p t e r和B r i d g e模式通常被用于软件生命周期的不同阶段。当你发现
两个不兼容的类必须同时工作时,就有必要使用A d a p t e r模式,其目的一般是为了避免代码重
复。此处耦合不可预见。相反, B r i d g e的使用者必须事先知道:一个抽象将有多个实现部分,
并且抽象和实现两者是独立演化的。A d a p t e r模式在类已经设计好后实施;而B r i d g e模式在设
计类之前实施。这并不意味着A d a p t e r模式不如B r i d g e模式,只是因为它们针对了不同的问题。
你可能认为f a c a d e (参见F a c a d e ( 4 . 5 ) )是另外一组对象的适配器。但这种解释忽视了一个事
实:即F a c a d e定义一个新的接口,而A d a p t e r则复用一个原有的接口。记住,适配器使两个已
有的接口协同工作,而不是定义一个全新的接口。
4.8.2 Composite、Decorator与Proxy
C o m p o s i t e ( 4 . 3 )模式和D e c o r a t o r ( 4 . 4 )模式具有类似的结构图,这说明它们都基于递归组合
来组织可变数目的对象。这一共同点可能会使你认为, d e c o r a t o r对象是一个退化的c o m p o s i t e,
但这一观点没有领会D e c o r a t o r模式要点。相似点仅止于递归组合,同样,这是因为这两个模
式的目的不同。
Decorator 旨在使你能够不需要生成子类即可给对象添加职责。这就避免了静态实现所有
功能组合,从而导致子类急剧增加。C o m p o s i t e则有不同的目的,它旨在构造类,使多个相关
的对象能够以统一的方式处理,而多重对象可以被当作一个对象来处理。它重点不在于修饰,
而在于表示。
尽管它们的目的截然不同,但却具有互补性。因此Composite 和D e c o r a t o r模式通常协同
使用。在使用这两种模式进行设计时,我们无需定义新的类,仅需将一些对象插接在一起即
可构建应用。这时系统中将会有一个抽象类,它有一些c o m p o s i t e子类和d e c o r a t o r子类,还有
一些实现系统的基本构建模块。此时, composites 和d e c o r a t o r将拥有共同的接口。从
D e c o r a t o r模式的角度看,c o m p o s i t e是一个C o n c r e t e C o m p o n e n t。而从c o m p o s i t e模式的角度看,
d e c o r a t o r则是一个L e a f。当然,他们不一定要同时使用,正如我们所见,它们的目的有很大
的差别。
另一种与D e c o r a t o r模式结构相似的模式是P r o x y ( 4 . 7 )。这两种模式都描述了怎样为对象提
供一定程度上的间接引用,proxy 和d e c o r a t o r对象的实现部分都保留了指向另一个对象的指针,
它们向这个对象发送请求。然而同样,它们具有不同的设计目的。
像D e c o r a t o r模式一样, Proxy 模式构成一个对象并为用户提供一致的接口。但与
D e c o r a t o r模式不同的是, Proxy 模式不能动态地添加或分离性质,它也不是为递归组合而设
计的。它的目的是,当直接访问一个实体不方便或不符合需要时,为这个实体提供一个替代
者,例如,实体在远程设备上,访问受到限制或者实体是持久存储的。
在P r o x y模式中,实体定义了关键功能,而P r o x y提供(或拒绝)对它的访问。在
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 4 5
递归组合成为D e c o r a t o r模式中一个必不可少的部分。而在P r o x y模式中则不是这样,因为
P r o x y模式强调一种关系( P r o x y与它的实体之间的关系),这种关系可以静态的表达。
模式间的这些差异非常重要,因为它们针对了面向对象设计过程中一些特定的经常发生
问题的解决方法。但这并不意味着这些模式不能结合使用。可以设想有一个p r o x y - d e c o r a t o r,
它可以给p r o x y添加功能,或是一个d e c o r a t o r- p r o x y用来修饰一个远程对象。尽管这种混合可
能有用(我们手边还没有现成的例子),但它们可以分割成一些有用的模式。
1 4 6 设计模式:可复用面向对象软件的基础
本文地址:http://com.8s8s.com/it/it3146.htm