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

    技术2022-05-11  141

    第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 y3. 动机当一个抽象可能有多个实现时,通常用继承来协调它们。抽象类定义对该抽象的接口,而具体的子类则用不同方式加以实现。但是此方法有时不够灵活。继承机制将抽象部分与它的实现部分固定在一起,使得难以对抽象部分和实现部分独立地进行修改、扩充和重用。让我们考虑在一个用户界面工具箱中,一个可移植的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 t1 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 r3. 动机有时我们希望给某个对象而不是整个类添加一些功能。例如,一个图形用户界面工具箱允许你对任意一个用户界面组件添加一些特性,例如边框,或是一些行为,例如窗口滚动。使用继承机制是添加功能的一种有效途径,从其他类继承过来的边框特性可以被多个子类的实例所使用。但这种方法不够灵活,因为边框的选择是静态的,用户不能控制对组件加边框的方式和时机。一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌入的对象为装饰。这个装饰与它所装饰的组件接口一致,因此它对使用该组件的客户透明。它将客户请求转发给该组件,并且可能在转发前后执行一些额外的动作(例如画一个边框)。透明性使得你可以递归的嵌套多个装饰,从而可以添加任意多的功能,如下图所示。例如,假定有一个对象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 eTe 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

    转载请注明原文地址: https://ibbs.8miu.com/read-4222.html
    最新回复(0)