第5章行为模式 行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述 它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流。它们将你的注意 力从控制流转移到对象间的联系方式上来。 行为类模式使用继承机制在类间分派行为。本章包括两个这样的模式。其中Te m p l a t e M e t h o d(5 . 1 0)较为简单和常用。模板方法是一个算法的抽象定义,它逐步地定义该算法, 每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。另一种行 为类模式是I n t e r p r e t e r(5 . 3)。它将一个文法表示为一个类层次,并实现一个解释器作为这些 类的实例上的一个操作。 行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样 相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象 如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在 极端情况下,每一个对象都要了解所有其他的对象。M e d i a t o r(5 . 5)在对等对象间引入一个 m e d i a t o r对象以避免这种情况的出现。m e d i a t o r提供了松耦合所需的间接性。 Chain of Responsibility(5.1)提供更松的耦合。它让你通过一条候选对象链隐式的向一个对 象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的, 你可以在运行时刻决定哪些候选者参与到链中。 O b s e r v e r ( 5 . 7 )模式定义并保持对象间的依赖关系。典型的O b s e r v e r的例子是Smalltalk 中的 模型/视图/控制器,其中一旦模型的状态发生变化,模型的所有视图都会得到通知。 其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。S t r a t e g y ( 5 . 9 )模式将 算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。C o m m a n d ( 5 . 2 )模式 将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其 他方式使用。S t a t e ( 5 . 8 )模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对 象可改变它的行为。Vi s i t o r ( 5 . 11 )封装分布于多个类之间的行为,而I t e r a t o r ( 5 . 4 )则抽象了访问 和遍历一个集合中的对象的方式。 5.1 CHAIN OF RESPONSIBILITY(职责链)-对象行为型模式 1. 意图 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这 些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 2. 动机 考虑一个图形用户界面中的上下文有关的帮助机制。用户在界面的任一部分上点击就可 以得到帮助信息,所提供的帮助依赖于点击的是界面的哪一部分以及其上下文。例如,对话 框中的按钮的帮助信息就可能和主窗口中类似的按钮不同。如果对那一部分界面没有特定的 帮助信息,那么帮助系统应该显示一个关于当前上下文的较一般的帮助信息-比如说,整个 对话框。 因此很自然地,应根据普遍性( g e n e r a l i t y )即从最特殊到最普遍的顺序来组织帮助信息。 而且,很明显,在这些用户界面对象中会有一个对象来处理帮助请求;至于是哪一个对象则取 决于上下文以及可用的帮助具体到何种程度。 这儿的问题是提交帮助请求的对象(如按钮)并不明确知道谁是最终提供帮助的对象。我们 要有一种办法将提交帮助请求的对象与可能提供帮助信息的对象解耦( d e c o u p l e )。Chain of R e s p o n s i b i l i t y模式告诉我们应该怎么做。 这一模式的想法是,给多个对象处理一个请求的机会,从而解耦发送者和接受者。该请 求沿对象链传递直至其中一个对象处理它,如下图所示。 从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候 选者。提交请求的对象并不明确地知道哪一个对象将会处理它-我们说该请求有一个隐式的 接收者(implicit receiver)。 假设用户在一个标有“P r i n t” 的按钮窗口组件上单击帮助,而该按钮包含在一个 P r i n t D i a l o g的实例中,该实例知道它所属的应用对象(见前面的对象框图)。下面的交互框图 (diagram) 说明了帮助请求怎样沿链传递: 在这个例子中,既不是aPrintButton 也不是aPrintDialog 处理该请求;它一直被传递给 a n A p p l i c a t i o n,anApplication 处理它或忽略它。提交请求的客户不直接引用最终响应它的对 象。 要沿链转发请求,并保证接收者为隐式的( i m p l i c i t ),每个在链上的对象都有一致的处理请 求和访问链上后继者的接口。例如,帮助系统可定义一个带有相应的HandleHelp 操作的 H e l p H a n d l e r类。HelpHandler 可为所有候选对象类的父类,或者它可被定义为一个混入 (m i x i n)类。这样想处理帮助请求的类就可将HelpHandler 作为其一个父类,如下页上图所示。 按钮、对话框,和应用类都使用HelpHandler 操作来处理帮助请求。H e l p H a n d l e r的 HandleHelp 操作缺省的是将请求转发给后继。子类可重定义这一操作以在适当的情况下提供 帮助;否则它们可使用缺省实现转发该请求。 3. 适用性 在以下条件下使用Responsibility 链: • 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。 • 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 • 可处理一个请求的对象集合应被动态指定。 4. 结构 一个典型的对象结构可能如下图所示: 5. 参与者 • H a n d l e r(如H e l p H a n d l e r) - 定义一个处理请求的接口。 - (可选) 实现后继链。 • C o n c r e t e H a n d l e r(如P r i n t B u t t o n和P r i n t D i a l o g) - 处理它所负责的请求。 - 可访问它的后继者。 - 如果可处理该请求,就处理之;否则将该请求转发给它的后继者。 • C l i e n t - 向链上的具体处理者( C o n c r e t e H a n d l e r )对象提交请求。 6. 协作 • 当客户提交一个请求时,请求沿链传递直至有一个ConcreteHandler 对象负责处理它。 7. 效果 Responsibility 链有下列优点和缺点( l i a b i l i t i e s ) : 1 ) 降低耦合度该模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需 知道该请求会被“正确”地处理。接收者和发送者都没有对方的明确的信息,且链中的对象 不需知道链的结构。 结果是,职责链可简化对象的相互连接。它们仅需保持一个指向其后继者的引用,而不 需保持它所有的候选接受者的引用。 2) 增强了给对象指派职责( R e s p o n s i b i l i t y )的灵活性当在对象中分派职责时,职责链给你 更多的灵活性。你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个 请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。 3) 不保证被接受既然一个请求没有明确的接收者,那么就不能保证它一定会被处理- 该请求可能一直到链的末端都得不到处理。一个请求也可能因该链没有被正确配置而得不到 处理。 8. 实现 下面是在职责链模式中要考虑的实现问题: 1) 实现后继者链有两种方法可以实现后继者链。 a) 定义新的链接(通常在H a n d l e r中定义,但也可由ConcreteHandlers 来定义)。 b) 使用已有的链接。 我们的例子中定义了新的链接,但你常常可使用已有的对象引用来形成后继者链。例如, 在一个部分-整体层次结构中,父构件引用可定义一个部件的后继者。窗口组件( Wi d g e t) 结构可能早已有这样的链接。C o m p o s i t e(4 . 3)更详细地讨论了父构件引用。 当已有的链接能够支持你所需的链时,完全可以使用它们。这样你不需要明确定义链接, 而且可以节省空间。但如果该结构不能反映应用所需的职责链,那么你必须定义额外的链接。 2) 连接后继者如果没有已有的引用可定义一个链,那么你必须自己引入它们。这种情 况下H a n d l e r 不仅定义该请求的接口,通常也维护后继链接。这样H a n d l e r 就提供了 H a n d l e R e q u e s t 的缺省实现: H a n d l e R e q u e s t 向后继者( 如果有的话) 转发请求。如果 ConcreteHandler 子类对该请求不感兴趣,它不需重定义转发操作,因为它的缺省实现进行无 条件的转发。 此处为一个H e l p H a n d l e r基类,它维护一个后继者链接: 3) 表示请求可以有不同的方法表示请求。最简单的形式,比如在H a n d l e H e l p的例子中, 请求是一个硬编码的(hard-coded) 操作调用。这种形式方便而且安全,但你只能转发H a n d l e r 类定义的固定的一组请求。 另一选择是使用一个处理函数,这个函数以一个请求码(如一个整型常数或一个字符串)为 参数。这种方法支持请求数目不限。唯一的要求是发送方和接受方在请求如何编码问题上应 达成一致。 这种方法更为灵活,但它需要用条件语句来区分请求代码以分派请求。另外,无法用类 型安全的方法来传递请求参数,因此它们必须被手工打包和解包。显然,相对于直接调用一 个操作来说它不太安全。 为解决参数传递问题,我们可使用独立的请求对象来封装请求参数。R e q u e s t类可明确地 描述请求,而新类型的请求可用它的子类来定义。这些子类可定义不同的请求参数。处理者 必须知道请求的类型(即它们正使用哪一个R e q u e s t子类)以访问这些参数。 为标识请求,R e q u e s t可定义一个访问器( a c c e s s o r )函数以返回该类的标识符。或者,如果 实现语言支持的话,接受者可使用运行时的类型信息。 以下为一个分派函数的框架( s k e t c h ),它使用请求对象标识请求。定义于基类R e q u e s t中的 G e t K i n d操作识别请求的类型: 子类可通过重定义H a n d l e R e q u e s t扩展该分派函数。子类只处理它感兴趣的请求;其他的 请求被转发给父类。这样就有效的扩展了(而不是重写) H a n d l e R e q u e s t操作。例如,一个 E x t e n d e d H a n d l e r子类扩展了M y H a n d l e r版本的H a n d l e R e q u e s t : 4) 在S m a l l t a l k中自动转发你可以使用Smalltalk 中的d o e s N o t U n d e r s t a n d机制转发请求。 没有相应方法的消息被doseNotUnderstand 的实现捕捉(trap in),此实现可被重定义,从而可 向一个对象的后继者转发该消息。这样就不需要手工实现转发;类仅处理它感兴趣的请求, 而依赖doesNotUnderstand 转发所有其他的请求。 9. 代码示例 下面的例子举例说明了在一个像前面描述的在线帮助系统中,职责链是如何处理请求的。 帮助请求是一个显式的操作。我们将使用在窗口组件层次中的已有的父构件引用来在链中的 窗口组件间传递请求,并且我们将在H a n d l e r类中定义一个引用以在链中的非窗口组件间传递 帮助请求。 HelpHandler 类定义了处理帮助请求的接口。它维护一个帮助主题(缺省值为空),并保持 对帮助处理对象链中它的后继者的引用。关键的操作是H a n d l e H e l p,它可被子类重定义。 HasHelp 是一个辅助操作,用于检查是否有一个相关的帮助主题。 所有的窗口组件都是Wi d g e t抽象类的子类。Wi d g e t是HelpHandler 的子类,因为所有的用 户界面元素都可有相关的帮助。(我们也可以使用另一种基于混入类的实现方式) 在我们的例子中,按钮是链上的第一个处理者。B u t t o n类是Wi d g e t类的子类。Button 构 造函数有两个参数: 对包含它的窗口组件的引用和其自身的帮助主题。 B u t t o n版本的H a n d l e H e l p首先测试检查其自身是否有帮助主题。如果开发者没有定义一个 帮助主题,就用H e l p H a n d l e r中的H a n d l e H e l p操作将该请求转发给它的后继者。如果有帮助主 题,那么就显示它,并且搜索结束。 D i a l o g实现了一个类似的策略,只不过它的后继者不是一个窗口组件而是任意的帮助请求 处理对象。在我们的应用中这个后继者将是A p p l i c a t i o n的一个实例。 在链的末端是A p p l i c a t i o n的一个实例。该应用不是一个窗口组件,因此A p p l i c a t i o n不是 H e l p H a n d l e r的直接子类。当一个帮助请求传递到这一层时,该应用可提供关于该应用的一般 性的信息,或者它可以提供一系列不同的帮助主题。 下面的代码创建并连接这些对象。此处的对话框涉及打印,因此这些对象被赋给与打印 相关的主题。 我们可对链上的任意对象调用H a n d l e H e l p以触发相应的帮助请求。要从按钮对象开始搜 索,只需对它调用H a n d l e H e l p : b u t t o n - > H a n d l e H e l p ( ) ; 在这种情况下,按钮会立即处理该请求。注意任何H e l p H a n d l e r类都可作为D i a l o g的后继 者。此外,它的后继者可以被动态地改变。因此不管对话框被用在何处,你都可以得到它正 确的与上下文相关的帮助信息。 10. 已知应用 许多类库使用职责链模式处理用户事件。对H a n d l e r类它们使用不同的名字,但思想是一样 的:当用户点击鼠标或按键盘,一个事件产生并沿链传播。MacApp[App89] 和E T + + [ W G M 8 8 ] 称之为“事件处理者”,S y m a n t e c的T C L库[ S y m 9 3 b ]称之为“B u r e a u c r a t”,而N e X T的A p p K i t命 名为“R e s p o n d e r”。 图形编辑器框架U n i d r a w定义了“命令” C o m m a n d对象,它封装了发给C o m p o n e n t和 C o m p o n e n t Vi e w对象[ V L 9 0 ]的请求。一个构件或构件视图可解释一个命令以进行一个操作, 这里“命令”就是请求。这对应于在实现一节中描述的“对象作为请求” 的方法。构件和构 件视图可以组织为层次式的结构。一个构件或构件视图可将命令解释转发给它的父构件,而 父构件依次可将它转发给它的父构件,如此类推,就形成了一个职责链。 E T + +使用职责链来处理图形的更新。当一个图形对象必须更新它的外观的一部分时,调 用I n v a l i d a t e R e c t操作。一个图形对象自己不能处理I n v a l i d a t e R e c t,因为它对它的上下文了解 不够。例如,一个图形对象可被包装在一些类似滚动条( S c r o l l e r s )或放大器( Z o o m e r s )的对象中, 这些对象变换它的坐标系统。那就是说,对象可被滚动或放大以至它有一部分在视区外。因 此缺省的I n v a l i d a t e R e c t的实现转发请求给包装的容器对象。转发链中的最后一个对象是一个 窗口( Wi n d o w )实例。当窗口收到请求时,保证失效矩形被正确变换。窗口通知窗口系统接口 并请求更新,从而处理I n v a l i d a t e R e c t。 11. 相关模式 职责链常与C o m p o s i t e(4 . 3)一起使用。这种情况下,一个构件的父构件可作为它的后继。 5.2 COMMAND(命令)-对象行为型模式 1. 意图 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队 或记录请求日志,以及支持可撤消的操作。 2. 别名 动作( A c t i o n ),事务( Tr a n s a c t i o n ) 3. 动机 有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。 例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱 不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪 个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。 命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。 这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的C o m m a n d类, 它定义了一个执行操作的接口。其最简单的形式是一个抽象的E x e c u t e操作。具体的C o m m a n d 子类将接收者作为其一个实例变量,并实现E x e c u t e操作,指定接收者采取的动作。而接收者 有执行该请求所需的具体信息。 用C o m m a n d对象可很容易的实现菜单( M e n u),每一菜单中的选项都是一个菜单项 (M e n u I t e m)类的实例。一个A p p l i c a t i o n类创建这些菜单和它们的菜单项以及其余的用户界面。 该A p p l i c a t i o n类还跟踪用户已打开的D o c u m e n t对象。 该应用为每一个菜单项配置一个具体的C o m m a n d子类的实例。当用户选择了一个菜单项 时,该M e n u I t e m对象调用它的C o m m a n d对象的E x e c u t e方法,而E x e c u t e执行相应操作。 M e n u I t e m对象并不知道它们使用的是C o m m a n d的哪一个子类。C o m m a n d子类里存放着请求的 接收者,而E x c u t e操作将调用该接收者的一个或多个操作。 例如,P a s t e C o m m a n d支持从剪贴板向一个文档( D o c u m e n t )粘贴正文。P a s t e C o m m a n d的接 收者是一个文档对象,该对象是实例化时提供的。E x e c u t e操作将调用该D o c u m e n t的P a s t e操 作。 而O p e n C o m m a n d的E x e c u t e操作却有所不同:它提示用户输入一个文档名,创建一个相应
的文档对象,将其入作为接收者的应用对象中,并打开该文档。 有时一个M e n u I t e m需要执行一系列命令。例如,使一个页面按正常大小居中的M e n u I t e m 可由一个C e n t e r D o c u m e n t C o m m a n d对象和一个N o r m a l S i z e C o m m a n d对象构建。因为这种需将 多条命令串接起来的情况很常见,我们定义一个M a c r o C o m m a n d类来让一个M e n u I t e m执行任 意数目的命令。M a c r o C o m m a n d是一个具体的C o m m a n d子类,它执行一个命令序列。 M a c r o C o m m a n d没有明确的接收者,而序列中的命令各自定义其接收者。 请注意这些例子中C o m m a n d模式是怎样解耦调用操作的对象和具有执行该操作所需信息 的那个对象的。这使我们在设计用户界面时拥有很大的灵活性。一个应用如果想让一个菜单 与一个按钮代表同一项功能,只需让它们共享相应具体C o m m a n d子类的同一个实例即可。我 们还可以动态地替换C o m m a n d对象,这可用于实现上下文有关的菜单。我们也可通过将几个 命令组成更大的命令的形式来支持命令脚本(command scripting)。所有这些之所以成为可能乃 是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将会被如何执行。 4. 适用性 当你有如下需求时,可使用C o m m a n d模式: • 像上面讨论的M e n u I t e m对象那样,抽象出待执行的动作以参数化某对象。你可用过程 语言中的回调(c a l l b a c k)函数表达这种参数化机制。所谓回调函数是指函数先在某处 注册,而它将在稍后某个需要的时候被调用。C o m m a n d模式是回调机制的一个面向对 象的替代品。 • 在不同的时刻指定、排列和执行请求。一个C o m m a n d对象可以有一个与初始请求无关 的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负 责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。 • 支持取消操作。C o m m a n d的E x c u t e操作可在实施操作前将状态存储起来,在取消操作时 这个状态用来消除该操作的影响。C o m m a n d接口必须添加一个U n e x e c u t e操作,该操作 取消上一次E x e c u t e调用的效果。执行的命令被存储在一个历史列表中。可通过向后和 向前遍历这一列表并分别调用U n e x e c u t e和E x e c u t e来实现重数不限的“取消”和“重 做”。 • 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在C o m m a n d接口中添 加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过 程包括从磁盘中重新读入记录下来的命令并用E x e c u t e操作重新执行它们。 • 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( t r a n s a c t i o n ) 的信息系统中很常见。一个事务封装了对数据的一组变动。C o m m a n d模式提供了对事 务进行建模的方法。C o m m a n d有一个公共的接口,使得你可以用同一种方式调用所有 的事务。同时使用该模式也易于添加新事务以扩展系统。 5. 结构 6. 参与者 • C o m m a n d - 声明执行操作的接口。 • C o n c r e t e C o m m a n d ( P a s t e C o m m a n d,O p e n C o m m a n d ) - 将一个接收者对象绑定于一个动作。 - 调用接收者相应的操作,以实现E x e c u t e。 • C l i e n t ( A p p l i c t i o n ) - 创建一个具体命令对象并设定它的接收者。 • Invoker ( M e n u I t e m ) - 要求该命令执行这个请求。 • R e c e i v e r ( D o c u m e n t,A p p l i c a t i o n ) - 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。 7. 协作 • C l i e n t创建一个C o n c r e t e C o m m a n d对象并指定它的R e c e i v e r对象。 • 某I n v o k e r对象存储该C o n c r e t e C o m m a n d对象。
• 该I n v o k e r通过调用C o m m a n d对象的E x e c u t e操作来提交一个请求。若该命令是可撤消的, C o n c r e t e C o m m a n d就在执行E x c u t e操作之前存储当前状态以用于取消该命令。 • ConcreteCommand对象对调用它的R e c e i v e r的一些操作以执行该请求。 下图展示了这些对象之间的交互。它说明了C o m m a n d是如何将调用者和接收者(以及它执 行的请求)解耦的。 8. 效果 C o m m a n d模式有以下效果: 1) Command模式将调用操作的对象与知道如何实现该操作的对象解耦。 2) Command是头等的对象。它们可像其他的对象一样被操纵和扩展。 3) 你可将多个命令装配成一个复合命令。例如是前面描述的M a c r o C o m m a n d类。一般说 来,复合命令是C o m p o s i t e模式的一个实例。 4) 增加新的C o m m a n d很容易,因为这无需改变已有的类。 9. 实现 实现C o m m a n d模式时须考虑以下问题: 1) 一个命令对象应达到何种智能程度命令对象的能力可大可小。一个极端是它仅确定 一个接收者和执行该请求的动作。另一极端是它自己实现所有功能,根本不需要额外的接收 者对象。当需要定义与已有的类无关的命令,当没有合适的接收者,或当一个命令隐式地知 道它的接收者时,可以使用后一极端方式。例如,创建另一个应用窗口的命令对象本身可能 和任何其他的对象一样有能力创建该窗口。在这两个极端间的情况是命令对象有足够的信息 可以动态的找到它们的接收者。 2 ) 支持取消( u n d o)和重做( r e d o) 如果C o m m a n d提供方法逆转( r e v e r s e )它们操作的执 行( 例如U n e x e c u t e 或U n d o 操作) ,就可支持取消和重做功能。为达到这个目的, C o n c r e t e C o m m a n d类可能需要存储额外的状态信息。这个状态包括: • 接收者对象,它真正执行处理该请求的各操作。 • 接收者上执行操作的参数。 • 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接 收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。 若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的 取消和重做,就需要有一个已被执行命令的历史表列(history list),该表列的最大长度决定了 取消和重做的级数。历史表列存储了已被执行的命令序列。向后遍历该表列并逆向执行 ( r e v e r s e - e x e c u t i n g )命令是取消它们的结果;向前遍历并执行命令是重执行它们。 有时可能不得不将一个可撤消的命令在它可以被放入历史列表中之前先拷贝下来。这是 因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间 会发生变化,那就必须进行拷贝以区分相同命令的不同调用。 例如,一个删除选定对象的删除命令( D e l e t e C o m m a n d )在它每次被执行时,必须存储不同 的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放入历史表列中。如 果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放入历史 表列中。在放入历史表列中之前必须被拷贝的那些C o m m a n d起着原型(参见P r o t o t y p e模式 (3 . 4))的作用。 3 ) 避免取消操作过程中的错误积累在实现一个可靠的、能保持原先语义的取消/重做机 制时,可能会遇到滞后影响问题。由于命令重复的执行、取消执行,和重执行的过程可能会 积累错误,以至一个应用的状态最终偏离初始值。这就有必要在C o m m a n d中存入更多的信息 以保证这些对象可被精确地复原成它们的初始状态。这里可使用M e m e n t o模式(5 . 6)来让该 C o m m a n d访问这些信息而不暴露其他对象的内部信息。 4) 使用C + +模板对( 1 )不能被取消( 2 )不需要参数的命令,我们可使用C + +模板来实现, 这样可以避免为每一种动作和接收者都创建一个C o m m a n d子类。我们将在代码示例一节说明 这种做法。 10. 代码示例 此处所示的C + +代码给出了动机一节中的C o m m a n d类的实现的大致框架。我们将定义 O p e n C o m m a n d、P a s t e C o m m a n d和M a c r o C o m m a n d。首先是抽象的C o m m a n d类: O p e n C o m m a n d打开一个名字由用户指定的文档。注意O p e n C o m m a n d的构造器需要一个 A p p l i c a t i o n对象作为参数。A s k U s e r是一个提示用户输入要打开的文档名的实现例程。 P a s t e C o m m a n d需要一个D o c u m e n t对象作为其接收者。该接收者将作为一个参数给 P a s t e C o m m a n d的构造器。 对于简单的不能取消和不需参数的命令, 可以用一个类模板来参数化该命令的接收者。我 们将为这些命令定义一个模板子类SimpleCommand. 用R e c e i v e r类型参数化S i m p l e C o m m a n d, 并维护一个接收者对象和一个动作之间的绑定,而这一动作是用指向一个成员函数的指针存储 的。 构造器存储接收者和对应实例变量中的动作。E x e c u t e操作实施接收者的这个动作。 为创建一个调用M y c l a s s类的一个实例上的A c t i o n的C o m m a n d对象, 仅需如下代码: 记住, 这一方案仅适用于简单命令。更复杂的命令不仅要维护它们的接收者,而且还要登 记参数,有时还要保存用于取消操作的状态。此时就需要定义一个C o m m a n d的子类。 M a c r o C o m m a n d管理一个子命令序列,它提供了增加和删除子命令的操作。这里不需要 显式的接收者,因为这些子命令已经定义了它们各自的接收者。 M a c r o C o m m a n d的关键是它的E x e c u t e成员函数。它遍历所有的子命令并调用其各自的 E x e c u t e操作。 注意,如果M a c r o C o m m a n d实现取消操作, 那么它的子命令必须以相对于E x e c u t e的实现相 反的顺序执行各子命令的取消操作。 最后, MacroCommand必须提供管理它的子命令的操作。M a c r o C o m m a n d也负责删除它的 子命令。 11. 已知应用 可能最早的命令模式的例子出现在L i e b e r m a n [ L i e 8 5 ]的一篇论文中。M a c A p p [ A p p 8 9 ]使实 现可撤消操作的命令这一说法被普遍接受。而E T + + [ W G M 8 8 ],I n t e r Vi e w s [ L C I + 9 2 ],和 U n i d r a w [ V L 9 0 ]也都定义了符合C o m m a n d模式的类。I n t e r Vi e w s定义了一个A c t i o n抽象类,它 提供命令功能。它还定义了一个A c t i o n C a l l b a c k模板,这个模板以A c t i o n方法为参数, 可自动 生成C o m m a n d子类。 T H I N K类库[ S y m 9 3 b ]也使用C o m m a n d模式支持可撤消的操作。T H I N K中的命令被称为 “任务”( Ta s k s )。任务对象沿着一个Chain of Responsiblity(5 . 1)传递以供消费( c o n s u m p t i o n )。 U n i d r a w的命令对象很特别,它的行为就像是一个消息。一个U n i d r a w命令可被送给另一 个对象去解释,而解释的结果因接收的对象而异。此外, 接收者可以委托另一个对象来进行解 释,典型的情况的是委托给一个较大的结构中(比如在一个职责链中)接收者的父构件。这样, U n i d r a w命令的接收者是计算出来的而不是预先存储的。U n i d r a w的解释机制依赖于运行时的 类型信息。 C o p l i e n在C + + [ C o p 9 2 ]中描述了C + +中怎样实现f u n c t o r s。F u n c t o r s是一种实际上是函数的 对象。他通过重载函数调用操作符(operator( ))达到了一定程度的使用透明性。命令模式不同, 它着重于维护接收者和函数(即动作)之间的绑定, 而不仅是维护一个函数。 12. 相关模式 C o m p o s i t e模式(4 . 3)可被用来实现宏命令。 M e m e n t o模式(5 . 6)可用来保持某个状态,命令用这一状态来取消它的效果。 在被放入历史表列前必须被拷贝的命令起到一种原型( 3 . 4 )的作用。 5.3 INTERPRETER(解释器)-类行为型模式 1. 意图 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示 来解释语言中的句子。 2. 动机 如果一种特定类型的问题发生的频率足够高, 那么可能就值得将该问题的各个实例表述为 一个简单语言中的句子。这样就可以构建一个解释器, 该解释器通过解释这些句子来解决该问 题。 例如,搜索匹配一个模式的字符串是一个常见问题。正则表达式是描述字符串模式的一 种标准语言。与其为每一个的模式都构造一个特定的算法,不如使用一种通用的搜索算法来 解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。 解释器模式描述了如何为简单的语言定义一个文法, 如何在该语言中表示一个句子, 以及 如何解释这些句子。在上面的例子中, 本设计模式描述了如何为正则表达式定义一个文法, 如 何表示一个特定的正则表达式, 以及如何解释这个正则表达式。 考虑以下文法定义正则表达式: 符号e x p r e s s i o n是开始符号, literal是定义简单字的终结符。 解释器模式使用类来表示每一条文法规则。在规则右边的符号是这些类的实例变量。上面的 文法用五个类表示: 一个抽象类R e g u l a r E x p r e s s i o n和它四个子类L i t e r a l E x p r e s s i o n、A l t e r n a t i o n E x p r e s s i o n、S e q u e n c e E x p r e s s i o n和R e p e t i t i o n E x p r e s s i o n后三个类定义的变量代表子表达式。 每个用这个文法定义的正则表达式都被表示为一个由这些类的实例构成的抽象语法树。 例如, 抽象语法树: 表示正则表达式: raining & (dogs | cats) * 如果我们为R e g u l a r E x p r e s s i o n的每一子类都定义解释( I n t e r p r e t )操作,那么就得到了为这 些正则表达式的一个解释器。解释器将该表达式的上下文做为一个参数。上下文包含输入字 符串和关于目前它已有多少已经被匹配等信息。为匹配输入字符串的下一部分,每一 R e g u l a r E x p r e s s i o n的子类都在当前上下文的基础上实现解释操作( I n t e r p r e t )。例如, • LiteralExpression将检查输入是否匹配它定义的字( l i t e r a l )。 • AlternationExpression将检查输入是否匹配它的任意一个选择项。 • RepetitionExpression将检查输入是否含有多个它所重复的表达式。 等等。 3. 适用性 当有一个语言需要解释执行, 并且你可将该语言中的句子表示为一个抽象语法树时,可使 用解释器模式。而当存在以下情况时该模式效果最好: • 该文法简单对于复杂的文法, 文法的类层次变得庞大而无法管理。此时语法分析程序生 成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式, 这样可以节 省空间而且还可能节省时间。 • 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的, 而是 首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种 情况下, 转换器仍可用解释器模式实现, 该模式仍是有用的。 4. 结构(见下页图) 5. 参与者 • A b s t r a c t E x p r e s s i o n (抽象表达式,如R e g u l a r E x p r e s s i o n ) - 声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。 • Te r m i n a l E x p r e s s i o n (终结符表达式,如L i t e r a l E x p r e s s i o n ) - 实现与文法中的终结符相关联的解释操作。 - 一个句子中的每个终结符需要该类的一个实例。 • N o n t e r m i n a l E x p r e s s i o n (非终结符表达式,如AlternationExpression, Repetition- Expression, SequenceExpressions) - 对文法中的每一条规则R ::= R1R2. . . Rn都需要一个N o n t e r m i n a l E x p r e s s i o n类。 - 为从R1到Rn的每个符号都维护一个A b s t r a c t E x p r e s s i o n类型的实例变量。 - 为文法中的非终结符实现解释( I n t e r p r e t )操作。解释( I n t e r p r e t )一般要递归地调用表示 R1到Rn的那些对象的解释操作。 • C o n t e x t(上下文) - 包含解释器之外的一些全局信息。 • C l i e n t(客户) - 构建(或被给定) 表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语 法树由N o n t e r m i n a l E x p r e s s i o n和Te r m i n a l E x p r e s s i o n的实例装配而成。 - 调用解释操作。 6. 协作 • C l i e n t构建(或被给定)一个句子, 它是N o n t e r m i n a l E x p r e s s i o n和Te r m i n a l E x p r e s s i o n的实例 的一个抽象语法树. 然后初始化上下文并调用解释操作。 • 每一非终结符表达式节点定义相应子表达式的解释操作。而各终结符表达式的解释操作 构成了递归的基础。 • 每一节点的解释操作用上下文来存储和访问解释器的状态。 7. 效果 解释器模式有下列的优点和不足: 1) 易于改变和扩展文法因为该模式使用类来表示文法规则, 你可使用继承来改变或扩展 该文法。已有的表达式可被增量式地改变,而新的表达式可定义为旧表达式的变体。 2) 也易于实现文法定义抽象语法树中各个节点的类的实现大体类似。这些类易于直接 编写,通常它们也可用一个编译器或语法分析程序生成器自动生成。 3) 复杂的文法难以维护解释器模式为文法中的每一条规则至少定义了一个类(使用B N F定 义的文法规则需要更多的类)。因此包含许多规则的文法可能难以管理和维护。可应用其他的设 计模式来缓解这一问题。但当文法非常复杂时, 其他的技术如语法分析程序或编译器生成器更为 合适。 4) 增加了新的解释表达式的方式解释器模式使得实现新表达式“计算”变得容易。例如, 你可以在表达式类上定义一个新的操作以支持优美打印或表达式的类型检查。如果你经常创建 新的解释表达式的方式, 那么可以考虑使用Vi s i t o r ( 5 . 11 )模式以避免修改这些代表文法的类。 8. 实现 I n t e r p r e t e r和C o m p o s i t e(4 . 3)模式在实现上有许多相通的地方。下面是I n t e r p r e t e r所要考 虑的一些特殊问题: 1) 创建抽象语法树解释器模式并未解释如何创建一个抽象的语法树。换言之, 它不涉及 语法分析。抽象语法树可用一个表驱动的语法分析程序来生成,也可用手写的(通常为递归下 降法) 语法分析程序创建,或直接由C l i e n t提供。 2) 定义解释操作并不一定要在表达式类中定义解释操作。如果经常要创建一种新的解 释器, 那么使用Vi s i t o r(5 . 11)模式将解释放入一个独立的“访问者” 对象更好一些。例如, 一个程序设计语言的会有许多在抽象语法树上的操作,比如类型检查、优化、代码生成,等 等。恰当的做法是使用一个访问者以避免在每一个类上都定义这些操作。 3) 与F l y w e i g h t模式共享终结符在一些文法中, 一个句子可能多次出现同一个终结符。此 时最好共享那个符号的单个拷贝。计算机程序的文法是很好的例子-每个程序变量在整个 代码中将会出现多次。在动机一节的例子中, 一个句子中终结符dog (由L i t e r a l E x p r e s s i o n类描 述)也可出现多次。 终结节点通常不存储关于它们在抽象语法树中位置的信息。在解释过程中,任何它们所 需要的上下文信息都由父节点传递给它们。因此在共享的(内部的)状态和传入的(外部的)状态 区分得很明确, 这就用到了F l y w e i g h t(4 . 6)模式。 例如,dog LiteralExpression的每一实例接收一个包含目前已匹配子串信息的上下文。且 每一个这样的L i t e r a l E x p r e s s i o n在它的解释操作中做同样一件事(它检查输入的下一部分是否 包含一个d o g)无论该实例出现在语法树的哪个位置。 9. 代码示例 下面是两个例子。第一个是S m a l l t a l k中一个完整的的例子, 用于检查一个序列是否匹配一 个正则表达式。第二个是一个用于求布尔表达式的值的C + +程序。 正则表达式匹配器检查一个字符串是否属于一个正则表达式定义的语言。正则表达式用 下列文法定义: 该文法对动机一节中的例子略做修改。因为符号“*” 在S m a l l t a l k中不能作为后缀运算 符。因此我们用r e p e a t取代之。例如, 正则表达式: ( ( 'dog' | 'cat' ) repeat &'weather') 匹配输入字符串“dog dog cat weather”。 为实现这个匹配器, 我们定义在( 5 . 3)页描述的五个类。类S e q u e n c e E x p r e s s i o n包含实例 变量expression 1和expression 2作为它在抽象语法树中的子结点; A l t e r n a t i o n E x p r e s s i o n用实例 变量altercative 1和altercative 2中存储它的选择支;而R e p e t i t i o n E x p r e s s i o n在它的实例变量 r e p e t i t i o n中保存它所重复的表达式。L i t e r a l E x p r e s s i o n有一个c o m p o n e n t s实例变量,它保存了 一系列对象(可能为一些字符)。这些表示必须匹配输入序列的字串(literal string)。 m a t c h :操作实现了该正则表达式的一个解释器。定义抽象语法树的每一个类都实现了这一 操作。它将i n p u t S t a t e作为一个参数, 表示匹配进程的当前状态,也就是读入的部分输入字符串。 这一状态由一个输入流集刻画, 表示该正则表达式目前所能接收的输入集(当前已识别出 的输入流, 这大致等价于记录等价的有限自动机可能处于的所有状态)。 当前状态对r e p e a t操作最为重要。例如, 如果正则表达式为: 'a' repeat 那么解释器可匹配“ a”, “a a”, “a a a”, 等等。如果它是 'a' repeat & 'bc' 那么可以匹配“a b c”, “a a b c”, “a a a b c”, 等等. 但如果正则表达式是 'a' repeat & 'abc' 那么用子表达式“‘a’ r e p e a t” 匹配输入“a a b c” 将产生两个输入流, 一个匹配了输入 的一个字符, 而另一个匹配了两个字符。只有接受一个字符的那个流会匹配剩余的“a b c”。 现在我们考虑m a t c h的定义: 对每一个类定义相应的正则表达式。S e q u e n c e E x p r e s s i o n匹配 其序列中的每一个子表达式。通常它将从它的i n p u t S t a t e中删除输入流。 一个A l t e r n a t i o n E x p r e s s i o n会返回一个状态, 该状态由两个选择项的状态的并组成。 A l t e r n a t i o n E x p r e s s i o n的match 的定义是 R e p e t i t i o n E x p r e s s i o n的m a t c h :操作寻找尽可能多的可匹配的状态: 它的输出通常比它的输入包含更多的状态, 因为R e p e t i t i o n E x p r e s s i o n可匹配输入的重复体 的一次、两次或多次出现。而输出状态要表示所有这些可能性以允许随后的正则表达式的元 素决定哪一个状态是正确的。 最后, LiteralExpression的m a t c h :对每一可能的输入流匹配它的组成部分。它仅保留那些获 得匹配的输入流: 其中n e x t Av a i l a b l e :消息推进输入流(即读入文字)。这是唯一一个推进输入流的m a t c h :操 作。注意返回的状态包含的是输入流的拷贝, 这就保证匹配一个l i t e r a l不会改变输入流。这一 点很重要,因为每个A l t e r n a t i o n E x p r e s s i o n的选择项看到的应该是相同的输入流。 现在我们已经定义了组成抽象语法树的各个类,下面说明怎样构建语法树。我们犯不着 为正则表达式写一个语法分析程序,而只要在R e g u l a r E x p r e s s i o n类上定义一些操作,就可以 “计算”一个S m a l l t a l k表达式,得到的结果就是对应于该正则表达式的一棵抽象语法树。这使 我们可以把S m a l l t a l k内置编译器当作一个正则表达式的语法分析程序来使用。 为构建抽象语法树, 我们需要将“|”、“r e p e a t”,和“&”定义为R e g u l a r E x p r e s s i o n上的操 作。这些操作在R e g u l a r E x p r e s s i o n类中定义如下: a s R E x p操作将把l i t e r a l s转化为R e g u l a r E x p r e s s i o n。这些操作在类S t r i n g中定义: 如果我们在类层次的更高层( S m a l l t a l k中的SequenceableCollection, Smalltalk/V 中的 I n d e x e d C o l l e c i o t n )中定义这些操作, 那么象A r r a y和O r d e r e d C o l l e c t i o n这样的类也有这些操作的 定义,这就使得正则表达式可以匹配任何类型的对象序列。 第二个例子是在C + +中实现的对布尔表达式进行操作和求值。在这个语言中终结符是布尔 变量, 即常量t r u e和f a l s e。非终结符表示包含运算符and, or和n o t的布尔表达式。文法定义如下: 为简单起见, 我们忽略了操作符的优先次序且假定由构造该语法树的对象负责处理这件事。 这里我们定义布尔表达式上的两个操作。第一个操作是求值( e v a l u a t e ),即在一个上下文 中求一个布尔表达式的值,当然,该上下文必须为每个变量都赋以一个“真”或“假”的布 尔值。第二个操作是替换(replace), 即用一个表达式来替换一个变量以产生一个新的布尔表达 式。替换操作说明了解释器模式不仅可以用于求表达式的值,而且还可用作其它用途。在这 个例子中, 它就被用来对表达式本身进行操作。 此处我们仅给出BooleanExp, Va r i a b l e E x p和A n d E x p类的细节。类O r E x p和N o t E x p与 A n d E x p相似。C o n s t a n t类表示布尔常量。 B o o l e a n E x p为所有定义一个布尔表达式的类定义了一个接口: 类C o n t e x t定义从变量到布尔值的一个映射, 这些布尔值我们可用C + +中的常量t r u e和f a l s e 来表示。C o n t e x t有以下接口: 一个Va r i a b l e E x p表示一个有名变量: 构造器将变量的名字作为参数: 求一个变量的值, 返回它在当前上下文中的值。 拷贝一个变量返回一个新的Va r i a b l e E x p : 1 6 8 设计模式:可复用面向对象软件的基础
在用一个表达式替换一个变量时, 我们检查该待替换变量是否就是本对象代表的变量: A n d E x p表示由两个布尔表达式与操作得到的表达式。 一个A n d E x p的值求是它的操作数的值的逻辑“与”。 A n d E x p的C o p y和R e p l a c e操作将递归调用它的操作数的C o p y和R e p l a c e操作: 现在我们可以定义布尔表达式 并对给定的以t r u e或f a l s e赋值的x和y求这个表达式值: 对x和y的这一赋值,求得该表达式值为t r u e。要对其它赋值情况求该表达式的值仅需改变 上下文对象即可。 最后, 我们可用一个新的表达式替换变量y,并重新求值: 这个例子说明了解释器模式一个很重要的特点: 可以用多种操作来“解释” 一个句子。 在为B o o l e a n E x p定义的三种操作中, Evaluate 最切合我们关于一个解释器应该做什么的想法 -即, 它解释一个程序或表达式并返回一个简单的结果。但是,替换操作也可被视为一个解 释器。这个解释器的上下文是被替换变量的名字和替换它的表达式, 而它的结果是一个新的表 达式。甚至拷贝也可被视为一个上下文为空的解释器。将替换和拷贝视为解释器可能有点怪, 因为它们仅仅是树上的基本操作。Vi s i t o r ( 5 . 11 )中的例子说明了这三个操作都可以被重新组织 为独立的“解释器”访问者, 从而显示了它们之间深刻的相似性。 解释器模式不仅仅是分布在一个使用C o m p o s i t e ( 4 . 3 )模式的类层次上的操作。我们之所以 认为E v a l u a t e是一个解释器, 是因为我们认为B o o l e a n E x p类层次表示一个语言。对于一个用于 表示汽车部件装配的类层次,即使它也使用复合模式,我们还是不太可能将We i g h t和C o p y这 样的操作视为解释器,因为我们不会把汽车部件当作一个语言。这是一个看问题的角度问题; 如果我们真有“汽车部件语言”的语法, 那么也许可以认为在那些部件上的操作是以某种方式 解释该语言。 10. 已知应用 解释器模式在使用面向对象语言实现的编译器中得到了广泛应用, 如S m a l l t a l k编译器。 S P E C Ta l k使用该模式解释输入文件格式的描述[ S z a 9 2 ]。Q O C A约束-求解工具使用它对约束 进行计算[ H H M V 9 2 ]。 在最宽泛的概念下(即, 分布在基于C o m p o s i t e ( 4 . 3 )模式的类层次上的一种操作), 几乎每个 使用复合模式的系统也都使用了解释器模式。但一般只有在用一个类层次来定义某个语言时, 才强调使用解释器模式。 11. 相关模式 C o m p o s i t e模式(4 . 3): 抽象语法树是一个复合模式的实例。 F l y w e i g h t模式(4 . 6):说明了如何在抽象语法树中共享终结符。 I t e r a t o r(5 . 4):解释器可用一个迭代器遍历该结构。 Vi s i t o r(5 . 11):可用来在一个类中维护抽象语法树中的各节点的行为。 5.4 ITERATOR(迭代器)-对象行为型模式 1. 意图 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 2. 别名 游标(C u r s o r)。 3. 动机 一个聚合对象, 如列表(list), 应该提供一种方法来让别人可以访问它的元素,而又不需暴 露它的内部结构. 此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以 预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有 时还可能需要在同一个表列上同时进行多个遍历。 迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历 从列表对象中分离出来并放入一个迭代器(i t e r a t o r)对象中。迭代器类定义了一个访问该列 表元素的接口。迭代器对象负责跟踪当前的元素; 即, 它知道哪些元素已经遍历过了。 例如, 一个列表(L i s t)类可能需要一个列表迭代器(L i s t I t e r a t o r), 它们之间的关系如下图: 在实例化列表迭代器之前,必须提供待遍历的列表。一旦有了该列表迭代器的实例,就 可以顺序地访问该列表的各个元素。C u r r e n t I t e m操作返回表列中的当前元素, First操作初始化 迭代器,使当前元素指向列表的第一个元素, Next操作将当前元素指针向前推进一步,指向下 一个元素, 而I s D o n e检查是否已越过最后一个元素,也就是完成了这次遍历。 将遍历机制与列表对象分离使我们可以定义不同的迭代器来实现不同的遍历策略,而无 需在列表接口中列举它们。例如, 过滤表列迭代器( F i l t e r i n g L i s t I t e r a t o r )可能只访问那些满足特 定过滤约束条件的元素。 注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其 他聚合结构。最好能有一种办法使得不需改变客户代码即可改变该聚合类。可以通过将迭代 器的概念推广到多态迭代(polymorphic iteration)来达到这个目标。 例如, 假定我们还有一个列表的特殊实现,比如说S k i p L i s t [ P u g 9 0 ]。S k i p L i s t是一种具有 类似于平衡树性质的随机数据结构。我们希望我们的代码对L i s t和S k i p L i s t对象都适用。 首先,定义一个抽象列表类A b s t r a c t L i s t,它提供操作列表的公共接口。类似地,我们也 需要一个抽象的迭代器类I t e r a t o r,它定义公共的迭代接口。然后我们可以为每个不同的列表 实现定义具体的I t e r a t o r子类。这样迭代机制就与具体的聚合类无关了。 余下的问题是如何创建迭代器。既然要使这些代码不依赖于具体的列表子类, 就不能仅仅 简单地实例化一个特定的类, 而要让列表对象负责创建相应的迭代器。这需要列表对象提供 C r e a t e I t e r a t o r这样的操作, 客户请求调用该操作以获得一个迭代器对象。 创建迭代器是一个Factory Method模式( 3 . 3)的例子。我们在这里用它来使得一个客户 可向一个列表对象请求合适的迭代器。Factory Method模式产生两个类层次, 一个是列表的, 一 个是迭代器的。CreateIterator “联系” 这两个类层次。 4. 适用性 迭代器模式可用来: • 访问一个聚合对象的内容而无需暴露它的内部表示。 • 支持对聚合对象的多种遍历。 • 为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。 5. 结构 6. 参与者 • I t e r a t o r(迭代器) - 迭代器定义访问和遍历元素的接口。 • C o n c r e t e I t e r a t o r(具体迭代器) - 具体迭代器实现迭代器接口。 - 对该聚合遍历时跟踪当前位置。 • A g g r e g a t e(聚合) - 聚合定义创建相应迭代器对象的接口。 • C o n c r e t e A g g r e g a t e(具体聚合) - 具体聚合实现创建相应迭代器的接口,该操作返回C o n c r e t e I t e r a t o r的一个适当的实例。 7. 协作 • ConcreteIterator跟踪聚合中的当前对象,并能够计算出待遍历的后继对象。 8. 效果 迭代器模式有三个重要的作用: 1 ) 它支持以不同的方式遍历一个聚合复杂的聚合可用多种方式进行遍历。例如, 代码生 成和语义检查要遍历语法分析树。代码生成可以按中序或者按前序来遍历语法分析树。迭代 器模式使得改变遍历算法变得很容易: 仅需用一个不同的迭代器的实例代替原先的实例即可。 你也可以自己定义迭代器的子类以支持新的遍历。 2) 迭代器简化了聚合的接口有了迭代器的遍历接口,聚合本身就不再需要类似的遍历 接口了。这样就简化了聚合的接口。 3) 在同一个聚合上可以有多个遍历每个迭代器保持它自己的遍历状态。因此你可以同 时进行多个遍历。 9. 实现 迭代器在实现上有许多变化和选择。下面是一些较重要的实现。实现迭代器模式时常常 需要根据所使用的语言提供的控制结构来进行权衡。一些语言(例如, CLU[LG86])甚至直接支 持这一模式。 1) 谁控制该迭代一个基本的问题是决定由哪一方来控制该迭代, 是迭代器还是使用该迭 代器的客户。当由客户来控制迭代时, 该迭代器称为一个外部迭代器(external iterator),而当 由迭代器控制迭代时, 该迭代器称为一个内部迭代器(internal iterator) 。使用外部迭代器的客 户必须主动推进遍历的步伐,显式地向迭代器请求下一个元素。相反地, 若使用内部迭代器, 客户只需向其提交一个待执行的操作,而迭代器将对聚合中的每一个元素实施该操作。 外部迭代器比内部迭代器更灵活。例如, 若要比较两个集合是否相等,这个功能很容易用 外部迭代器实现,而几乎无法用内部迭代器实现。在象C + +这样不提供匿名函数、闭包, 或象 S m a l l t a l k和CLOS 这样不提供连续( c o n t i n u a t i o n )的语言中,内部迭代器的弱点更为明显。但另 一方面, 内部迭代器的使用较为容易, 因为它们已经定义好了迭代逻辑。 2) 谁定义遍历算法迭代器不是唯一可定义遍历算法的地方。聚合本身也可以定义遍历 算法,并在遍历过程中用迭代器来存储当前迭代的状态。我们称这种迭代器为一个游标 (cursor), 因为它仅用来指示当前位置。客户会以这个游标为一个参数调用该聚合的N e x t操作, 而N e x t操作将改变这个指示器的状态。 如果迭代器负责遍历算法, 那么将易于在相同的聚合上使用不同的迭代算法, 同时也易于 在不同的聚合上重用相同的算法。从另一方面说, 遍历算法可能需要访问聚合的私有变量。如 果这样,将遍历算法放入迭代器中会破坏聚合的封装性。 3) 迭代器健壮程度如何在遍历一个聚合的同时更改这个聚合可能是危险的。如果在遍 B o o c h分别称外部和内部迭代器为主动( a c t i v e )和被动( p a s s i v e )迭代器[ B o o 9 4 ]。“主动”和“被动”两个词 描述了客户的作用, 而不是指迭代器主动与否。 指示器是M e m e n t o模式的一个简单例子并且有许多和它相同的实现问题。 历聚合的时候增加或删除该聚合元素, 可能会导致两次访问同一个元素或者遗漏掉某个元素。 一个简单的解决办法是拷贝该聚合,并对该拷贝实施遍历, 但一般来说这样做代价太高。 一个健壮的迭代器(robust iterator)保证插入和删除操作不会干扰遍历, 且不需拷贝该聚合。 有许多方法来实现健壮的迭代器。其中大多数需要向这个聚合注册该迭代器。当插入或删除 元素时,该聚合要么调整迭代器的内部状态, 要么在内部的维护额外的信息以保证正确的遍 历。 K o f l e r在E T + + [ K o f 9 3 ]中对如何实现健壮的迭代器做了很充分的讨论。M u r r a y讨论了如何 为USL StandardComponents 列表类实现健壮的迭代器[ M u r 9 3 ]。 4) 附加的迭代器操作迭代器的最小接口由F i r s t、N e x t、I s D o n e和C u r r e n t I t e m 操作组成。 其他一些操作可能也很有用。例如, 对有序的聚合可用一个P r e v i o u s操作将迭代器定位到前一 个元素。S k i p To操作用于已排序并做了索引的聚合中,它将迭代器定位到符合指定条件的元 素对象上。 5) 在C + +中使用多态的迭代器使用多态迭代器是有代价的。它们要求用一个F a c t o r y M e t h o d动态的分配迭代器对象。因此仅当必须多态时才使用它们。否则使用在栈中分配内存 的具体的迭代器。 多态迭代器有另一个缺点: 客户必须负责删除它们。这容易导致错误, 因为你容易忘记释 放一个使用堆分配的迭代器对象,当一个操作有多个出口时尤其如此。而且其间如果有异常 被触发的话,迭代器对象将永远不会被释放。 P r o x y(4 . 4)模式提供了一个补救方法。我们可使用一个栈分配的P r o x y作为实际迭代器 的中间代理。该代理在其析构器中删除该迭代器。这样当该代理生命周期结束时,实际迭代 器将同它一起被释放。即使是在发生异常时,该代理机制能保证正确地清除迭代器对象。这 就是著名的C + +“资源分配即初始化”技术[ E S 9 0 ]的一个应用。下面的代码示例给出了一个例 子。 6) 迭代器可有特权访问迭代器可被看为创建它的聚合的一个扩展。迭代器和聚合紧密 耦合。在C + +中我们可让迭代器作为它的聚合的一个友元( f r i e n d )来表示这种紧密的关系。这 样你就不需要在聚合类中定义一些仅为迭代器所使用的操作。 但是, 这样的特权访问可能使定义新的遍历变得很难, 因为它将要求改变该聚合的接口增 加另一个友元。为避免这一问题, 迭代器类可包含一些p r o t e c t e d操作来访问聚合类的重要的非 公共可见的成员。迭代器子类(且只有迭代器子类)可使用这些p r o t e c t e d操作来得到对该聚合的 特权访问。 7) 用于复合对象的迭代器在C o m p o s i t e ( 4 . 3 )模式中的那些递归聚合结构上, 外部迭代器 可能难以实现, 因为在该结构中不同对象处于嵌套聚合的多个不同层次, 因此一个外部迭代 器为跟踪当前的对象必须存储一条纵贯该C o m p o s i t e的路径。有时使用一个内部迭代器会更容 易一些。它仅需递归地调用自己即可,这样就隐式地将路径存储在调用栈中,而无需显式地 维护当前对象位置。 如果复合中的节点有一个接口可以从一个节点移到它的兄弟节点、父节点和子节点, 那么 基于游标的迭代器是个更好的选择。游标只需跟踪当前的节点; 它可依赖这种节点接口来遍历 甚至可以将N e x t,I s D o n e和C u r r e n t I t e m并入到一个操作中,该操作前进到下一个对象并返回这个对象,如果 遍历结束,那么这个操作返回一个特定的值(例如,0 )标志该迭代结束。这样我们就使这个接口变得更小了。 该复合对象。 复合常常需要用多种方法遍历。前序, 后序, 中序以及广度优先遍历都是常用的。你可用 不同的迭代器类来支持不同的遍历。 8) 空迭代器一个空迭代器( N u l l I t e r a t o r )是一个退化的迭代器, 它有助于处理边界条件。 根据定义,一个N u l l I t e r a t o r总是已经完成了遍历:即, 它的I s D o n e操作总是返回t r u e。 空迭代器使得更容易遍历树形结构的聚合(如复合对象)。在遍历过程中的每一节点, 都可 向当前的元素请求遍历其各个子结点的迭代器。该聚合元素将返回一个具体的迭代器。但叶 节点元素返回N u l l I t e r a t o r的一个实例。这就使我们可以用一种统一的方式实现在整个结构上 的遍历。 10. 代码示例 我们将看看一个简单L i s t类的实现, 它是我们的基础库(附录C )的一部分。我们将给出两个 迭代器的实现, 一个以从前到后的次序遍历该表列, 而另一个以从后到前的次序遍历(基础库只 支持第一种)。然后我们说明如何使用这些迭代器,以及如何避免限定于一种特定的实现。在 此之后, 我们将改变原来的设计以保证迭代器被正确的删除。最后一个例子示例一个内部迭代 器并与其相应的外部迭代器进行比较。 1) 列表和迭代器接口首先让我们看与实现迭代器相关的部分L i s t接口。完整的接口请参 考附录C。 该L i s t类通过它的公共接口提供了一个合理的有效的途径以支持迭代。它足以实现这两种 遍历。因此没有必再要给迭代器对底层数据结构的访问特权,也就是说,迭代器类不是列表 的友元。为确保对不同遍历的透明使用, 我们定义一个抽象的迭代器类, 它定义了迭代器接口。 2) 迭代器子类的实现列表迭代器是迭代器的一个子类。 L i s t I t e r a t o r的实现简单直接。它存储L i s t和列表当前位置的索引_ c u r r e n t。 F i r s t将迭代器置于第一个元素: N e x t使当前元素向前推进一步: I s D o n e检查指向当前元素的索引是否超出了列表: 最后, CurrentItem 返回当前索引指向的元素。若迭代已经终止, 则抛出一个I t e r a t o r O u t O f B o u n d s异常: R e v e r s e L i s t I t e r a t o r的实现是几乎是一样的,只不过它的F i r s t操作将_ c u r r e n t置于列表的末 尾, 而N e x t操作将_ c u r r e n t减一,向表头的方向前进一步。 3 ) 使用迭代器假定有一个雇员(E m p l o y e e)对象的List, 而我们想打印出列表包含的所 有雇员的信息。E m p l o y e e类用一个P r i n t操作来打印本身的信息。为打印这个列表, 我们定义一 个P r i n t E m p l o y e e操作, 此操作以一个迭代器为参数,并使用该迭代器遍历和打印这个列表: 前面我们已经实现了从后向前和从前向后两种遍历的迭代器, 我们可用这个操作以两种次 序打印雇员信息: 4) 避免限定于一种特定的列表实现考虑一个L i s t的变体s k i p l i s t会对迭代代码产生什么影 响。L i s t的S k i p L i s t子类必须提供一个实现I t e r a t o r接口的相应的迭代器S k i p L i s t I t e r a t o r。在内 部, 为了进行高效的迭代, S k i p L i s t I t e r a t o r必须保持多个索引。既然S k i p L i s t I t e r a t o r实现了 I t e r a t o r,P r i n t E m p l o y e e操作也可用于用S k i p L i s t存储的雇员列表。 尽管这种方法是可行的,但最好能够无需明确指定具体的L i s t实现(此处即为S k i p L i s t)。 为此可以引入一个A b s t r a c t L i s t类,它为不同的列表实现给出一个标准接口。L i s t和S k i p L i s t成 为A b s t r a c t L i s t的子类。 为支持多态迭代, A b s t r a c t L i s t定义一个Factory Method,称为C r e a t e I t e r a t o r。各个列表子 类重定义这个方法以返回相应的迭代器。 另一个办法是定义一个一般的m i x i n类Traversable, 它定义一个用于创建迭代器接口。聚合 类通过混入(继承)Tr a v e r s a b l e来支持多态迭代。 L i s 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对象: 现在我们可以写出不依赖于具体列表表示的打印雇员信息的代码。 5) 保证迭代器被删除注意C r e a t e C r e a t e I t e r a t o r返回的是一个动态分配的迭代器对象。在 使用完毕后,必须删除这个迭代器,否则会造成内存泄漏。为方便客户, 我们提供一个 I t e r a t o r P t r作为迭代器的代理, 这个机制可以保证在I t e r a t o r对象离开作用域时清除它。 I t e r a t o r P t r总是在栈上分配。C + +自动调用它的析构器,而该析构器将删除真正的迭代 器。I t e r a t o r P t r重载了操作符“- >”和“*”,使得可将I t e r a t o r P t r用作一个指向迭代器的指针。 I t e r a t o r P t r的成员都实现为内联的, 这样它们不会产生任何额外开销。 你只需定义私有的n e w和d e l e t e操作符即可在编译时保证这一点。不需要附加的实现。 I t e r a t o r P t r简化了打印代码: 6) 一个内部的ListIterator 最后,让我们看看一个内部的或被动的L i s t I t e r a t o r类是怎么实 现的。此时由迭代器来控制迭代, 并对列表中的每一个元素施行同一个操作。 问题是如何实现一个抽象的迭代器,可以支持不同的作用于列表各个元素的操作。有些 语言支持所谓的匿名函数或闭包,使用这些机制可以较方便地实现抽象的迭代器。但是C + + 并不支持这些机制。此时,至少有两种办法可供选择: (1)给迭代器传递一个函数指针(全局的 或静态的)。( 2 )依赖于子类生成。在第一种情况下, 迭代器在迭代过程中的每一步调用传递给 它的操作,在第二种情况下, 迭代器调用子类重定义了的操作以实现一个特定的行为。 这两种选择都不是尽善尽美。常常需要在迭代时累积( a c c u m u l a t e )状态,而用函数来实现 这个功能并不太适合; 因为我们将不得不使用静态变量来记住这个状态。I t e r a t o r子类给我们提 供了一个方便的存储累积状态的地方,比如存放在一个实例变量中。但为每一个不同的遍历 创建一个子类需要做更多的工作。 下面是第二种实现办法的一个大体框架, 它利用了子类生成。这里我们称内部迭代器为一 个L i s t Tr a v e r s e r。 L i s t Tr a v e r s e r以一个L i s t实例为参数。在内部, 它使用一个外部L i s t I t e r a t o r进行遍历。 Tr a v e r s e启动遍历并对每一元素项调用P r o c e s s I t e m操作。内部迭代器可在某次P r o c e s s I t e m操作 返回f a l s e时提前终止本次遍历。而Tr a v e r s e返回一个布尔值指示本次遍历是否提前终止。 让我们使用一个L i s t Tr a v e r s e r来打印雇员列表中的头1 0个雇员。为达到这个目的, 必须定 义一个L i s t Tr a v e r s e r的子类并重定义其P r o c e s s I t e m操作。我们用一个_ c o u n t实例变量中对已打 印的雇员进行计数。 下面是P r i n t N E m p l o y e e s怎样打印列表中的头1 0个雇员的代码: 注意这里客户不需要说明如何进行迭代循环。整个迭代逻辑可以重用。这是内部迭代器 的主要优点。但其实现比外部迭代器要复杂一些, 因为必须定义一个新的类。与使用外部迭 代器比较: 内部迭代器可以封装不同类型的迭代。例如, FilteringListTr a v e r s e r封装的迭代仅处理能通 过测试的那些列表元素: 这个类接口除了增加了用于测试的成员函数Te s t I t e m外与L i s t Tr a v e r s e r相同,它的子类将重 定义Te s t I t e m以指定所需的测试。 Tr a v e r s e根据测试的结果决定是否越过当前元素继续遍历: 这个类的一中变体是让Tr a v e r s e返回值指示是否至少有一个元素通过测试。 11. 已知应用 迭代器在面向对象系统中很普遍。大多数集合类库都以不同的形式提供了迭代器。 这里是一个流行的集合类库-B o o c h构件[ B o o 9 4 ]中的一个例子,该类库提供了一个队 列的两种实现:固定大小的(有界的)实现和动态增长的(无界的)实现。队列的接口由一个抽象 的Q u e u e类定义。为了支持不同队列实现上的多态迭代,队列迭代器的实现基于抽象的Q u e u e 类接口。这样做的优点在于,不需要每个队列都实现一个Factory Method来提供合适的迭代器。 但是, 它要求抽象Q u e u e类的接口的功能足够强大以有效地实现通用迭代器。 在S m a l l t a l k中不需显式定义迭代器。标准的集合类(包, 集合, 字典, 有序集, 字符串, 等等) 都定义一个内部迭代器方法d o :,它以一个程序块(即闭包)为参数。集合中的每个元素先被绑 定于与程序块中的局部变量,然后该程序块被执行。S m a l l t a l k也包括一些S t r e a m类,这些 S t r e a m类支持一个类似于迭代器的接口。R e a d S t r e a m实质上是一个迭代器, 而且对所有的顺序 集合它都可作为一个外部迭代器。对于非顺序的集合类如集合和字典没有标准的外部迭代器。 在这些例子中的Tr a v e r s e操作是一个带原语操作Te s t I t e m和P r o c e s s I t e m的Template Method。(5 . 1 0) E T + +容器类[ W G M 8 8 ]提供了前面讨论的多态迭代器和负责清除迭代器的P r o x y。U n i d r a w 图形编辑框架使用基于指示器的迭代器[ V L 9 0 ]。 O b j e c t Wi n d o w 2 . 0 [ B o r 9 4 ]为容器提供了一个迭代器类层次。你可对不同的容器类型用相同 的方法迭代。O b j e c t Wi n d o w迭代语法靠重载算后增量算符+ +推进迭代。 12. 相关模式 C o m p o s i t e ( 4 . 3 ):迭代器常被应用到象复合这样的递归结构上。 Factory Method(3.3):多态迭代器靠Factory Method来例化适当的迭代器子类。 M e m e n t o ( 5 . 6 ):常与迭代器模式一起使用。迭代器可使用一个m e m e n t o来捕获一个迭代的 状态。迭代器在其内部存储m e m e n t o。 5.5 MEDIATOR(中介者)-对象行为型模式 1. 意图 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从 而使其耦合松散,而且可以独立地改变它们之间的交互。 2. 动机 面向对象设计鼓励将行为分布到各个对象中。这种分布可能会导致对象间有许多连接。 在最坏的情况下,每一个对象都知道其他所有对象。 虽然将一个系统分割成许多对象通常可以增强可复用性, 但是对象间相互连接的激增又会 降低其可复用性。大量的相互连接使得一个对象似乎不太可能在没有其他对象的支持下工作 -系统表现为一个不可分割的整体。而且,对系统的行为进行任何较大的改动都十分困难, 因为行为被分布在许多对象中。结果是, 你可能不得不定义很多子类以定制系统的行为。 例如,考虑一个图形用户界面中对话框的实现。对话框使用一个窗口来展现一系列的窗 口组件, 如按钮、菜单和输入域等, 如下图所示。 通常对话框中的窗口组件间存在依赖关系。例如, 当一个特定的输入域为空时, 某个按钮 不能使用;在称为列表框的一列选项中选择一个表目可能会改变一个输入域的内容;反过来, 在输入域中输入正文可能会自动的选择一个或多个列表框中相应的表目;一旦正文出现在输 入域中, 其他一些按钮可能就变得能够使用了,这些按钮允许用户做一些操作, 比如改变或删 除这些正文所指的东西。 不同的对话框会有不同的窗口组件间的依赖关系。因此即使对话框显示相同类型的窗口 组件, 也不能简单地直接重用已有的窗口组件类; 而必须定制它们以反映特定对话框的依赖关 系。由于涉及很多个类,用逐个生成子类的办法来定制它们会很冗长。 可以通过将集体行为封装在一个单独的中介者( m e d i a t o r )对象中以避免这个问题。中介者 负责控制和协调一组对象间的交互。中介者充当一个中介以使组中的对象不再相互显式引用。 这些对象仅知道中介者, 从而减少了相互连接的数目。 例如, F o n t D i a l o g D i r e c t o r可作为一个对话框中的窗口组件间的中介者。F o n t D i a l o g D i r e c t o r 对象知道对话框中的各窗口组件,并协调它们之间的交互。它充当窗口组件间通信的中转中心, 如下图所示。 下面的交互图说明了各对象如何协作处理一个列表框中选项的变化。 下面一系列事件使一个列表框的选择被传送给一个输入域: 1 ) 列表框告诉它的操作者它被改变了。 2 ) 导控者从列表框中得到选中的选择项。 3) 导控者将该选择项传递给入口域。 4 ) 现在入口域已有正文, 导控者使得用于发起一个动作(如“半黑体” ,“斜体”)的某个 (某些)按钮可用。 注意导控者是如何在对话框和入口域间进行中介的。窗口组件间的通信都通过导控者间 接地进行。它们不必互相知道; 它们仅需知道导控者。而且,由于所有这些行为都局部于一个 类中,只要扩展或替换这个类, 就可以改变和替换这些行为。 这里展示的是F o n t D i a l o g D i r e c t o r抽象怎样被集成到一个类库中,如下图所示。 D i a l o g D i r e c t o r是一个抽象类, 它定义了一个对话框的总体行为。客户调用S h o w D i a l o g操 作将对话框显示在屏幕上。C r e a t e Wi d g e t s是创建一个对话框的窗口组件的抽象操作。 Wi d g e t C h a n g e d是另一个抽象操作; 窗口组件调用它来通知它的导控者它们被改变了。 D i a l o g D i r e c t o r的子类将重定义C r e a t e Wi d g e t s以创建正确的窗口组件, 并重定义Wi d g e t C h a n g e d 以处理其变化。 3. 适用性 在下列情况下使用中介者模式: • 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理 解。 • 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。 • 想定制一个分布在多个类中的行为,而又不想生成太多的子类。 4. 结构 一个典型的对象结构可能如下页图所示。 5. 参与者 • M e d i a t o r(中介者,如D i a l o g D i r e c t o r ) - 中介者定义一个接口用于与各同事( C o l l e a g u e)对象通信。 • C o n c r e t e M e d i a t o r(具体中介者,如F o n t D i a l o g D i r e c t o r ) - 具体中介者通过协调各同事对象实现协作行为。 - 了解并维护它的各个同事。 • Colleague class(同事类,如ListBox, EntryField) - 每一个同事类都知道它的中介者对象。 - 每一个同事对象在需与其他的同事通信的时候,与它的中介者通信。 6. 协作 • 同事向一个中介者对象发送和接收请求。中介者在各同事间适当地转发请求以实现协作 行为。 7. 效果 中介者模式有以下优点和缺点: 1) 减少了子类生成M e d i a t o r将原本分布于多个对象间的行为集中在一起。改变这些行为 只需生成M e d i t a t o r的子类即可。这样各个C o l l e a g u e类可被重用。 2) 它将各C o l l e a g u e解耦M e d i a t o r有利于各C o l l e a g u e间的松耦合. 你可以独立的改变和复 用各C o l l e a g u e类和M e d i a t o r类。 3 ) 它简化了对象协议用M e d i a t o r和各C o l l e a g u e间的一对多的交互来代替多对多的交互。 一对多的关系更易于理解、维护和扩展。 4)它对对象如何协作进行了抽象将中介作为一个独立的概念并将其封装在一个对象中, 使你将注意力从对象各自本身的行为转移到它们之间的交互上来。这有助于弄清楚一个系统 中的对象是如何交互的。 5) 它使控制集中化中介者模式将交互的复杂性变为中介者的复杂性。因为中介者封装 了协议, 它可能变得比任一个C o l l e a g u e都复杂。这可能使得中介者自身成为一个难于维护的 庞然大物。 8. 实现 下面是与中介者模式有关的一些实现问题: 1 ) 忽略抽象的M e d i a t o r类当各C o l l e a g u e仅与一个M e d i a t o r一起工作时, 没有必要定义一 个抽象的M e d i a t o r类。M e d i a t o r类提供的抽象耦合已经使各C o l l e a g u e可与不同的M e d i a t o r子类 一起工作, 反之亦然。 2 ) C o l l e a g u e-M e d i a t o r通信当一个感兴趣的事件发生时, Colleague必须与其M e d i a t o r 通信。一种实现方法是使用O b s e r v e r ( 5 . 7 )模式,将M e d i a t o r实现为一个O b s e r v e r,各C o l l e a g u e 作为S u b j e c t,一旦其状态改变就发送通知给M e d i a t o r。M e d i a t o r作出的响应是将状态改变的结 果传播给其他的C o l l e a g u e。 另一个方法是在M e d i a t o r中定义一个特殊的通知接口,各C o l l e a g u e在通信时直接调用该接 口。Wi n d o w s下的S m a l l t a l k / V使用某种形式的代理机制: 当与M e d i a t o r通信时, Colleague将自 身作为一个参数传递给M e d i a t o r,使其可以识别发送者。代码示例一节使用这种方法。而 S m a l l t a l k / V的实现方法将稍后在已知应用一节中讨论。 9. 代码示例 我们将使用一个D i a l o g D i r e c t o r来实现在动机一节中所示的字体对话框。抽象类 D i a l o g D i r e c t o r为导控者定义了一个接口。 Wi d g e t是窗口组件的抽象基类。一个窗口组件知道它的导控者。 C h a n g e d调用导控者的Wi d g e t C h a n g e d操作。通知导控者某个重要事件发生了。 D i a l o g D i r e c t o r的子类重定义Wi d g e t C h a n g e d以导控相应的窗口组件。窗口组件把对自身 的一个引用作为Wi d g e t C h a n g e d的参数,使得导控者可以识别哪个窗口组件改变了。 D i a l o g D i r e c t o r子类重定义纯虚函数C r e a t e Wi d g e t s,在对话框中构建窗口组件。 L i s t B o x、E n t r y F i e l d和B u t t o n是Wi d g e t的子类,用作特定的用户界面构成元素。L i s t B o x 提供了一个G e t S e l e c t i o n操作来得到当前的选择项, 而E n t r y F i e l d的S e t Te x t操作则将新的正文放 入该域中。 B u t t o n是一个简单的窗口组件, 它一旦被按下就调用C h a n g e d。这是在其H a n d l e M o u s e的实 现中完成的: F o n t D i a l o g D i r e c t a t o r类在对话框中的窗口组件间进行中介。F o n t D i a l o g D i r e c t a t o r是 D i a l o g D i r e c t o r的子类: F o n t D i a l o g D i r e c t o r跟踪它显示的窗口组件。它重定义C r e a t e Wi d g e t s以创建窗口组件并初 始化对它们的引用: Wi d g e t C h a n g e d保证窗口组件正确地协同工作: Wi d g e t C h a n g e d的复杂度随对话框的复杂度的增加而增加。在实践中,大对话框并不受欢 迎,其原因是多方面的,其中一个重要原因是中介者的复杂性可能会抵消该模式在其他方面 的带来的好处。 10. 已知应用 E T + + [ W G M 8 8 ]和THINK C类库[ S y m 9 3 b ]都在对话框中使用类似导控者的对象作为窗口 组件间的中介者。 Wi n d o w s下的S m a l l t a l k / V的应用结构基于中介者结构[ L a L 9 4 ]。在这个环境中, 一个应用由 一个包含一组窗格( p a n e )的窗口组成。该类库包含若干预定义的P a n e对象; 比如说Te x t P a n e、 L i s t B o x、Button, 等等。这些窗格无需继承即可直接使用。应用开发者仅需由Vi e w M a n a g e r衍 生子类, Vi e w M a n a g e r类负责窗格间的协调工作。Vi e w M a n a g e r是一个中介者,而每一个窗格 只知道它的view manager, 它被看作该窗格的“主人”。窗格不直接互相引用。 下面的对象图显示了一个应用运行时刻的情景。 S m a l l t a l k / V的P a n e - Vi e w M a n a g e r通信使用一种事件机制。当一个窗格想从中介者得到信 息或当它想通知中介者一些重要的事情发生时, 它产生一个事件。事件定义一个符号(如 # s e l e c t )来标识该事件。为处理该事件,视管理者为该窗格注册一个候选方法。这个方法是该 事件的处理程序; 一旦该事件发生它就会被调用。 下面的代码片段说明了在一个Vi e w M a n a g e r子类中,一个L i s t P a n e对象如何被创建以及 Vi e w M a n a g e r如何为# s e l e c t事件注册一个事件处理程序: 另一个中介者模式的应用是用于协调复杂的更新。一个例子是在O b s e r v e r ( 5 . 7 )中提到的 C h a n g e M a n a g e r类。C h a n g e M a n a g e r在s u b j e c t和O b s e r v e r间进行协调以避免冗余的更新。当一
个对象改变时, 它通知C h a n g e M a n a g e r, ChangeManager随即通知依赖于该对象的那些对象以协 调这个更新。 一个类似的应用出现在U n i d r a w绘图框架[ V L 9 0 ]中,它使用一个称为C S o l v e r的类来实现” 连接器”间的连接约束。图形编辑器中的对象可用不同的方式表现出相互依附。连接器用于 自动维护连接的应用中, 如框图编辑器和电路设计系统。C S o l v e r是连接器间的中介者。它解 释连接约束并更新连接器的位置以反映这些约束。 11. 相关模式 F a c a d e ( 4 . 5 )与中介者的不同之处在于它是对一个对象子系统进行抽象,从而提供了一个 更为方便的接口。它的协议是单向的,即F a c a d e对象对这个子系统类提出请求,但反之则不 行。相反, M e d i a t o r提供了各C o l l e a g u e对象不支持或不能支持的协作行为,而且协议是多向 的。 C o l l e a g u e可使用O b s e r v e r ( 5 . 7 )模式与M e d i a t o r通信。

|