设计模式可复用面向对象软件的基础 第5章 行为模式.docx
《设计模式可复用面向对象软件的基础 第5章 行为模式.docx》由会员分享,可在线阅读,更多相关《设计模式可复用面向对象软件的基础 第5章 行为模式.docx(62页珍藏版)》请在冰豆网上搜索。
设计模式可复用面向对象软件的基础第5章行为模式
第5章行为模式
行为模式涉及到算法和对象间职责的分配。
行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。
这些模式刻划了在运行时难以跟踪的复杂的控制流。
它们将你的注意力从控制流转移到对象间的联系方式上来。
行为类模式使用继承机制在类间分派行为。
本章包括两个这样的模式。
其中TemplateMethod(5.10)较为简单和常用。
模板方法是一个算法的抽象定义,它逐步地定义该算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。
另一种行为类模式是Interpreter(5.3)。
它将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的一个操作。
行为对象模式使用对象复合而不是继承。
一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。
这里一个重要的问题是对等的对象如何互相了解对方。
对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。
在极端情况下,每一个对象都要了解所有其他的对象。
Mediator(5.5)在对等对象间引入一个mediator对象以避免这种情况的出现。
mediator提供了松耦合所需的间接性。
ChainofResponsibility(5.1)提供更松的耦合。
它让你通过一条候选对象链隐式的向一个对象发送请求。
根据运行时刻情况任一候选者都可以响应相应的请求。
候选者的数目是任意的,你可以在运行时刻决定哪些候选者参与到链中。
Observer(5.7)模式定义并保持对象间的依赖关系。
典型的Observer的例子是Smalltalk中的模型/视图/控制器,其中一旦模型的状态发生变化,模型的所有视图都会得到通知。
其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。
Strategy(5.9)模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。
Command(5.2)模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其他方式使用。
State(5.8)模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对象可改变它的行为。
Visitor(5.11)封装分布于多个类之间的行为,而Iterator(5.4)则抽象了访问和遍历一个集合中的对象的方式。
5.1CHAINOFRESPONSIBILITY(职责链)—对象行为型模式
1.意图
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
2.动机
考虑一个图形用户界面中的上下文有关的帮助机制。
用户在界面的任一部分上点击就可以得到帮助信息,所提供的帮助依赖于点击的是界面的哪一部分以及其上下文。
例如,对话框中的按钮的帮助信息就可能和主窗口中类似的按钮不同。
如果对那一部分界面没有特定的帮助信息,那么帮助系统应该显示一个关于当前上下文的较一般的帮助信息—比如说,整个对话框。
因此很自然地,应根据普遍性(generality)即从最特殊到最普遍的顺序来组织帮助信息。
而且,很明显,在这些用户界面对象中会有一个对象来处理帮助请求;至于是哪一个对象则取决于上下文以及可用的帮助具体到何种程度。
这儿的问题是提交帮助请求的对象(如按钮)并不明确知道谁是最终提供帮助的对象。
我们要有一种办法将提交帮助请求的对象与可能提供帮助信息的对象解耦(decouple)。
ChainofResponsibility模式告诉我们应该怎么做。
这一模式的想法是,给多个对象处理一个请求的机会,从而解耦发送者和接受者。
该请求沿对象链传递直至其中一个对象处理它,如下图所示。
从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者。
提交请求的对象并不明确地知道哪一个对象将会处理它—我们说该请求有一个隐式的接收者(implicitreceiver)。
假设用户在一个标有“Print”的按钮窗口组件上单击帮助,而该按钮包含在一个PrintDialog的实例中,该实例知道它所属的应用对象(见前面的对象框图)。
下面的交互框图(diagram)说明了帮助请求怎样沿链传递:
在这个例子中,既不是aPrintButton也不是aPrintDialog处理该请求;它一直被传递给anApplication,anApplication处理它或忽略它。
提交请求的客户不直接引用最终响应它的对象。
要沿链转发请求,并保证接收者为隐式的(implicit),每个在链上的对象都有一致的处理请
求和访问链上后继者的接口。
例如,帮助系统可定义一个带有相应的HandleHelp操作的
HelpHandler类。
HelpHandler可为所有候选对象类的父类,或者它可被定义为一个混入(mixin)类。
这样想处理帮助请求的类就可将HelpHandler作为其一个父类,如下页上图所示。
按钮、对话框,和应用类都使用HelpHandler操作来处理帮助请求。
HelpHandler的
HandleHelp操作缺省的是将请求转发给后继。
子类可重定义这一操作以在适当的情况下提供帮助;否则它们可使用缺省实现转发该请求。
3.适用性在以下条件下使用Responsibility链:
•有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
•你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
•可处理一个请求的对象集合应被动态指定。
4.结构
一个典型的对象结构可能如下图所示:
5.参与者
•Handler(如HelpHandler)—定义一个处理请求的接口。
—(可选)实现后继链。
•ConcreteHandler(如PrintButton和PrintDialog)—处理它所负责的请求。
—可访问它的后继者。
—如果可处理该请求,就处理之;否则将该请求转发给它的后继者。
•Client
—向链上的具体处理者(ConcreteHandler)对象提交请求。
6.协作
•当客户提交一个请求时,请求沿链传递直至有一个ConcreteHandler对象负责处理它。
7.效果Responsibility链有下列优点和缺点(liabilities):
1)降低耦合度该模式使得一个对象无需知道是其他哪一个对象处理其请求。
对象仅需
知道该请求会被“正确”地处理。
接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。
结果是,职责链可简化对象的相互连接。
它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。
2)增强了给对象指派职责(Responsibility)的灵活性当在对象中分派职责时,职责链给你更多的灵活性。
你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。
你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。
3)不保证被接受既然一个请求没有明确的接收者,那么就不能保证它一定会被处理—该请求可能一直到链的末端都得不到处理。
一个请求也可能因该链没有被正确配置而得不到处理。
8.实现下面是在职责链模式中要考虑的实现问题:
1)实现后继者链有两种方法可以实现后继者链。
a)定义新的链接(通常在Handler中定义,但也可由ConcreteHandlers来定义)。
b)使用已有的链接。
我们的例子中定义了新的链接,但你常常可使用已有的对象引用来形成后继者链。
例如,在一个部分—整体层次结构中,父构件引用可定义一个部件的后继者。
窗口组件(Widget)结构可能早已有这样的链接。
Composite(4.3)更详细地讨论了父构件引用。
当已有的链接能够支持你所需的链时,完全可以使用它们。
这样你不需要明确定义链接,而且可以节省空间。
但如果该结构不能反映应用所需的职责链,那么你必须定义额外的链接。
2)连接后继者如果没有已有的引用可定义一个链,那么你必须自己引入它们。
这种情况下Handler不仅定义该请求的接口,通常也维护后继链接。
这样Handler就提供了HandleRequest的缺省实现:
HandleRequest向后继者(如果有的话)转发请求。
如果ConcreteHandler子类对该请求不感兴趣,它不需重定义转发操作,因为它的缺省实现进行无条件的转发。
此处为一个HelpHandler基类,它维护一个后继者链接:
3)表示请求可以有不同的方法表示请求。
最简单的形式,比如在HandleHelp的例子中,请求是一个硬编码的(hard-coded)操作调用。
这种形式方便而且安全,但你只能转发Handler类定义的固定的一组请求。
另一选择是使用一个处理函数,这个函数以一个请求码(如一个整型常数或一个字符串)为参数。
这种方法支持请求数目不限。
唯一的要求是发送方和接受方在请求如何编码问题上应达成一致。
这种方法更为灵活,但它需要用条件语句来区分请求代码以分派请求。
另外,无法用类型安全的方法来传递请求参数,因此它们必须被手工打包和解包。
显然,相对于直接调用一个操作来说它不太安全。
为解决参数传递问题,我们可使用独立的请求对象来封装请求参数。
Request类可明确地描述请求,而新类型的请求可用它的子类来定义。
这些子类可定义不同的请求参数。
处理者必须知道请求的类型(即它们正使用哪一个Request子类)以访问这些参数。
为标识请求,Request可定义一个访问器(accessor)函数以返回该类的标识符。
或者,如果实现语言支持的话,接受者可使用运行时的类型信息。
以下为一个分派函数的框架(sketch),它使用请求对象标识请求。
定义于基类Request中的GetKind操作识别请求的类型:
子类可通过重定义HandleRequest扩展该分派函数。
子类只处理它感兴趣的请求;其他的请求被转发给父类。
这样就有效的扩展了(而不是重写)HandleRequest操作。
例如,一个ExtendedHandler子类扩展了MyHandler版本的HandleRequest:
4)在Smalltalk中自动转发你可以使用Smalltalk中的doesNotUnderstand机制转发请求。
没有相应方法的消息被doseNotUnderstand的实现捕捉(trapin),此实现可被重定义,从而可向一个对象的后继者转发该消息。
这样就不需要手工实现转发;类仅处理它感兴趣的请求,而依赖doesNotUnderstand转发所有其他的请求。
9.代码示例
下面的例子举例说明了在一个像前面描述的在线帮助系统中,职责链是如何处理请求的。
帮助请求是一个显式的操作。
我们将使用在窗口组件层次中的已有的父构件引用来在链中的窗口组件间传递请求,并且我们将在Handler类中定义一个引用以在链中的非窗口组件间传递帮助请求。
HelpHandler类定义了处理帮助请求的接口。
它维护一个帮助主题(缺省值为空),并保持对帮助处理对象链中它的后继者的引用。
关键的操作是HandleHelp,它可被子类重定义。
HasHelp是一个辅助操作,用于检查是否有一个相关的帮助主题。
所有的窗口组件都是Widget抽象类的子类。
Widget是HelpHandler的子类,因为所有的用户界面元素都可有相关的帮助。
(我们也可以使用另一种基于混入类的实现方式)在我们的例子中,按钮是链上的第一个处理者。
Button类是Widget类的子类。
Button构造函数有两个参数:
对包含它的窗口组件的引用和其自身的帮助主题。
Button版本的HandleHelp首先测试检查其自身是否有帮助主题。
如果开发者没有定义一个帮助主题,就用HelpHandler中的HandleHelp操作将该请求转发给它的后继者。
如果有帮助主题,那么就显示它,并且搜索结束。
Dialog实现了一个类似的策略,只不过它的后继者不是一个窗口组件而是任意的帮助请求处理对象。
在我们的应用中这个后继者将是Application的一个实例。
在链的末端是Application的一个实例。
该应用不是一个窗口组件,因此Application不是HelpHandler的直接子类。
当一个帮助请求传递到这一层时,该应用可提供关于该应用的一般性的信息,或者它可以提供一系列不同的帮助主题。
下面的代码创建并连接这些对象。
此处的对话框涉及打印,因此这些对象被赋给与打印相关的主题。
我们可对链上的任意对象调用HandleHelp以触发相应的帮助请求。
要从按钮对象开始搜索,只需对它调用HandleHelp:
button->HandleHelp();
在这种情况下,按钮会立即处理该请求。
注意任何HelpHandler类都可作为Dialog的后继者。
此外,它的后继者可以被动态地改变。
因此不管对话框被用在何处,你都可以得到它正确的与上下文相关的帮助信息。
10.已知应用
许多类库使用职责链模式处理用户事件。
对Handler类它们使用不同的名字,但思想是一样的:
当用户点击鼠标或按键盘,一个事件产生并沿链传播。
MacApp[App89]和ET++[WGM88]称之为“事件处理者”,Symantec的TCL库[Sym93b]称之为“Bureaucrat”,而NeXT的AppKit命名为“Responder”。
图形编辑器框架Unidraw定义了“命令”Command对象,它封装了发给Component和ComponentView对象[VL90]的请求。
一个构件或构件视图可解释一个命令以进行一个操作,这里“命令”就是请求。
这对应于在实现一节中描述的“对象作为请求”的方法。
构件和构件视图可以组织为层次式的结构。
一个构件或构件视图可将命令解释转发给它的父构件,而父构件依次可将它转发给它的父构件,如此类推,就形成了一个职责链。
ET++使用职责链来处理图形的更新。
当一个图形对象必须更新它的外观的一部分时,调用InvalidateRect操作。
一个图形对象自己不能处理InvalidateRect,因为它对它的上下文了解不够。
例如,一个图形对象可被包装在一些类似滚动条(Scrollers)或放大器(Zoomers)的对象中,这些对象变换它的坐标系统。
那就是说,对象可被滚动或放大以至它有一部分在视区外。
因此缺省的InvalidateRect的实现转发请求给包装的容器对象。
转发链中的最后一个对象是一个窗口(Window)实例。
当窗口收到请求时,保证失效矩形被正确变换。
窗口通知窗口系统接口并请求更新,从而处理InvalidateRect。
11.相关模式职责链常与Composite(4.3)一起使用。
这种情况下,一个构件的父构件可作为它的后继。
5.2COMMAND(命令)—对象行为型模式
1.意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
2.别名动作(Action),事务(Transaction)
3.动机
有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。
例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。
但工具箱不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。
而工具箱的设计者无法知道请求的接受者或执行的操作。
命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。
这个对象可被存储并像其他的对象一样被传递。
这一模式的关键是一个抽象的Command类,它定义了一个执行操作的接口。
其最简单的形式是一个抽象的Execute操作。
具体的Command子类将接收者作为其一个实例变量,并实现Execute操作,指定接收者采取的动作。
而接收者有执行该请求所需的具体信息。
用Command对象可很容易的实现菜单(Menu),每一菜单中的选项都是一个菜单项(MenuItem)类的实例。
一个Application类创建这些菜单和它们的菜单项以及其余的用户界面。
该Application类还跟踪用户已打开的Document对象。
该应用为每一个菜单项配置一个具体的Command子类的实例。
当用户选择了一个菜单项
时,该MenuItem对象调用它的Command对象的Execute方法,而Execute执行相应操作。
MenuItem对象并不知道它们使用的是Command的哪一个子类。
Command子类里存放着请求的
接收者,而Excute操作将调用该接收者的一个或多个操作。
例如,PasteCommand支持从剪贴板向一个文档(Document)粘贴正文。
PasteCommand的接收者是一个文档对象,该对象是实例化时提供的。
Execute操作将调用该Document的Paste操作。
而OpenCommand的Execute操作却有所不同:
它提示用户输入一个文档名,创建一个相应
的文档对象,将其入作为接收者的应用对象中,并打开该文档。
有时一个MenuItem需要执行一系列命令。
例如,使一个页面按正常大小居中的MenuItem可由一个CenterDocumentCommand对象和一个NormalSizeCommand对象构建。
因为这种需将多条命令串接起来的情况很常见,我们定义一个MacroCommand类来让一个MenuItem执行任意数目的命令。
MacroCommand是一个具体的Command子类,它执行一个命令序列。
MacroCommand没有明确的接收者,而序列中的命令各自定义其接收者。
请注意这些例子中Command模式是怎样解耦调用操作的对象和具有执行该操作所需信息的那个对象的。
这使我们在设计用户界面时拥有很大的灵活性。
一个应用如果想让一个菜单与一个按钮代表同一项功能,只需让它们共享相应具体Command子类的同一个实例即可。
我们还可以动态地替换Command对象,这可用于实现上下文有关的菜单。
我们也可通过将几个命令组成更大的命令的形式来支持命令脚本(commandscripting)。
所有这些之所以成为可能乃是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将会被如何执行。
4.适用性当你有如下需求时,可使用Command模式:
•像上面讨论的MenuItem对象那样,抽象出待执行的动作以参数化某对象。
你可用过程语言中的回调(callback)函数表达这种参数化机制。
所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。
Command模式是回调机制的一个面向对象的替代品。
•在不同的时刻指定、排列和执行请求。
一个Command对象可以有一个与初始请求无关的生存期。
如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
•支持取消操作。
Command的Excute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。
Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。
执行的命令被存储在一个历史列表中。
可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“取消”和“重做”。
•支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。
在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。
从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。
•用构建在原语操作上的高层操作构造一个系统。
这样一种结构在支持事务(transaction)的信息系统中很常见。
一个事务封装了对数据的一组变动。
Command模式提供了对事务进行建模的方法。
Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。
同时使用该模式也易于添加新事务以扩展系统。
5.结构
6.参与者
•Command
—声明执行操作的接口。
•ConcreteCommand(Pas