操作型模式
第20章 操作(行为)模式
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上来。\r\n行为类模式使用继承机制在类间分派行为。其中Te m p l a t e M e t h o d较为简单和常用。模板方法是一个算法的抽象定义,它逐步地定义该算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。另一种行为类模式是I n t e r p r e t e r。它将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的一个操作。
行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在极端情况下,每一个对象都要了解所有其他的对象。M e d i a t o r在对等对象间引入一个m e d i a t o r对象以避免这种情况的出现。m e d i a t o r提供了松耦合所需的间接性。
Chain of Responsibility提供更松的耦合。它让你通过一条候选对象链隐式的向一个对象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的,你可以在运行时刻决定哪些候选者参与到链中。\r\n\r\nO b s e r v e r 模式定义并保持对象间的依赖关系。典型的O b s e r v e r的例子是Smalltalk 中的模型/视图/控制器,其中一旦模型的状态发生变化,模型的所有视图都会得到通知。
第22章 STATE(状态) —对象行为型模式
1. 意图\r\n 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。\r\n2. 别名\r\n 状态对象( Objects for States)\r\n3. 动机\r\n 考虑一个表示网络连接的类T C P C o n n e c t i o n。一个T C P C o n n e c t i o n对象的状态处于若干不同状态之一: 连接已建立( E s t a b l i s h e d)、正在监听( L i s t e n i n g )、连接已关闭( C l o s e d )。当一个T C P C o n n e c t i o n对象收到其他对象的请求时, 它根据自身的当前状态作出不同的反应。例如,一个O p e n请求的结果依赖于该连接是处于连接已关闭状态还是连接已建立状态。S t a t e模式描述了T C P C o n n e c t i o n如何在每一种状态下表现出不同的行为。
这一模式的关键思想是引入了一个称为T C P S t a t e的抽象类来表示网络的连接状态。T C P S t a t e类为各表示不同的操作状态的子类声明了一个公共接口。T C P S t a t e的子类实现与特定状态相关的行为。例如, TCPEstablished和T C P C l o s e d类分别实现了特定于T C P C o n n e c t i o n的连接已建立状态和连接已关闭状态的行为。
T C P C o n n e c t i o n类维护一个表示T C P连接当前状态的状态对象(一个T C P S t a t e子类的实例)。T C P C o n n e c t i o n类将所有与状态相关的请求委托给这个状态对象。T C P C o n n e c t i o n使用它的T C P S t a t e子类实例来执行特定于连接状态的操作。\r\n 一旦连接状态改变, T C P C o n n e c t i o n对象就会改变它所使用的状态对象。例如当连接从已建立状态转为已关闭状态时, TCPConnection 会用一个T C P C l o s e d的实例来代替原来的T C P E s t a b l i s h e d的实例。
4. 适用性\r\n在下面的两种情况下均可使用S t a t e模式:\r\n? 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。\r\n? 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。S t a t e模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5. 结构
6. 参与者\r\n? C o n t e x t(环境,如T C P C o n n e c t i o n )\r\n— 定义客户感兴趣的接口。\r\n— 维护一个C o n c r e t e S t a t e子类的实例,这个实例定义当前状态。\r\n? S t a t e(状态,如T C P S t a t e )\r\n— 定义一个接口以封装与C o n t e x t的一个特定状态相关的行为。\r\n? ConcreteState subclasses(具体状态子类,如TCPEstablished, TCPListen, TCPClosed)\r\n— 每一子类实现一个与C o n t e x t的一个状态相关的行为。
7. 协作\r\n? C o n t e x t将与状态相关的请求委托给当前的C o n c r e t e S t a t e对象处理。\r\n? C o n t e x t可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要时可访问C o n t e x t。\r\n? C o n t e x t是客户使用的主要接口。客户可用状态对象来配置一个C o n t e x t,一旦一个C o n t e x t配置完毕, 它的客户不再需要直接与状态对象打交道。\r\n? C o n t e x t或C o n c r e t e S t a t e子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条件下进行状态转换。
8. 效果
S t a t e模式有下面一些效果:\r\n1 ) 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来;\r\n2) 它使得状态转换显式化;\r\n3) State对象可被共享;
9. 实现
实现S t a t e模式有多方面的考虑:\r\n1 ) 谁定义状态转换 S t a t e模式不指定哪一个参与者定义状态转换准则。如果该准则是固定的, 那么它们可在C o n t e x t中完全实现。然而若让S t a t e子类自身指定它们的后继状态以及何时进行转换, 通常更灵活更合适。这需要C o n t e x t增加一个接口, 让S t a t e对象显式地设定C o n t e x t的当前状态。\r\n 用这种方法分散转换逻辑可以很容易地定义新的S t a t e子类来修改和扩展该逻辑。这样做的一个缺点是,一个S t a t e子类至少拥有一个其他子类的信息, 这就再各子类之间产生了实现依赖。
2) 基于表的另一种方法在C Programming Style[Car92]中, Carg i l描述了另一种将结构加载在状态驱动的代码上的方法: 他使用表将输入映射到状态转换。对每一个状态, 一张表将每一个可能的输入映射到一个后继状态。实际上, 这种方法将条件代码(和S t a t e模式下的虚函数)映射为一个查找表。
表的主要好处是它们的规则性: 你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而它也有一些缺点:\r\n? 对表的查找通常不如(虚)函数调用效率高。\r\n? 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。\r\n? 通常难以加入伴随状态转换的一些动作。\r\n但必须扩充这个机制以便在每一个转换上能够进行任意的计算。表驱动的状态机和S t a t e模式的主要区别可以被总结如下: State模式对与状态相关的行为进行建模, 而表驱动的方法着重于定义状态转换。
3 ) 创建和销毁S t a t e对象一个常见的值得考虑的实现上的权衡是, 究竟是( 1 )仅当需要S t a t e对象时才创建它们并随后销毁它们,还是( 2 )提前创建它们并且始终不销毁它们。\r\n4 ) 使用动态继承改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的办法实现, 但这在大多数面向对象程序设计语言中都是不可能的。
10. 代码示例
下面的例子给出了在动机一节描述的T C P连接例子的C 代码。这个例子是T C P协议的一个简化版本,它并未完整描述T C P连接的协议及其所有状态。
11. 已知应用
J o h n s o n和Z w e i g [ J Z 9 1 ]描述了S t a t e模式以及它在T C P连接协议上的应用。\r\n大多数流行的交互式绘图程序提供了以直接操纵的方式进行工作的“工具”。例如, 一个画直线的工具可以让用通过户点击和拖动来创建一条新的直线;一个选择工具可以让用户选择某个图形对象。通常有许多这样的工具放在一个选项板供用户选择。用户认为这一活动是选择一个工具并使用它, 但实际上编辑器的行为随当前的工具而变: 当一个绘制工具被激活时,我们创建图形对象;当选择工具被激活时, 我们选择图形对象;等等。我们可以使用S t a t e模式来根据当前的工具改变编辑器的行为。
12. 相关模式
F l y w e i g h t模式解释了何时以及怎样共享状态对象。\r\n 状态对象通常是S i n g l e t o n 。
第24章 命令模式
1. 意图\r\n 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。\r\n2. 别名\r\n 动作( A c t i o n ),事务( Tr a n s a c t i o n )
3. 动机\r\n有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。\r\n例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。
命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的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. 适用性\r\n当你有如下需求时,可使用C o m m a n d模式:\r\n? 像上面讨论的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对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。\r\n支持取消操作。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. 参与者\r\n? C o m m a n d\r\n— 声明执行操作的接口。\r\n? 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 )\r\n— 将一个接收者对象绑定于一个动作。\r\n— 调用接收者相应的操作,以实现E x e c u t e。\r\n? C l i e n t ( A p p l i c t i o n )\r\n— 创建一个具体命令对象并设定它的接收者。\r\n? Invoker ( M e n u I t e m )\r\n— 要求该命令执行这个请求。\r\n? 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 )\r\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对象。\r\n? 某I n v o k e r对象存储该C o n c r e t e C o m m a n d对象。\r\n? 该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操作之前存储当前状态以用于取消该命令。\r\n? ConcreteCommand对象对调用它的R e c e i v e r的一些操作以执行该请求。
图展示了这些对象之间的交互。它说明了C o m m a n d是如何将调用者和接收者(以及它执行的请求)解耦的。
8. 效果
C o m m a n d模式有以下效果:\r\n1) Command模式将调用操作的对象与知道如何实现该操作的对象解耦。\r\n2) Command是头等的对象。它们可像其他的对象一样被操纵和扩展。\r\n3) 你可将多个命令装配成一个复合命令。例如是前面描述的M a c r o C o m m a n d类。一般说来,复合命令是C o m p o s i t e模式的一个实例。\r\n4) 增加新的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类可能需要存储额外的状态信息。这个状态包括:\r\n? 接收者对象,它真正执行处理该请求的各操作。\r\n? 接收者上执行操作的参数。\r\n? 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。\r\n若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的取消和重做,就需要有一个已被执行命令的历史表列(history list),该表列的最大长度决定了取消和重做的级数。历史表列存储了已被执行的命令序列。向后遍历该表列并逆向执行( r e v e r s e - e x e c u t i n g )命令是取消它们的结果;向前遍历并执行命令是重执行它们。
3 ) 避免取消操作过程中的错误积累在实现一个可靠的、能保持原先语义的取消/重做机制时,可能会遇到滞后影响问题。由于命令重复的执行、取消执行,和重执行的过程可能会积累错误,以至一个应用的状态最终偏离初始值。这就有必要在C o m m a n d中存入更多的信息以保证这些对象可被精确地复原成它们的初始状态。这里可使用M e m e n t o模式来让该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,并维护一个接收者对象和一个动作之间的绑定,而这一动作是用指向一个成员函数的指针存储的。
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子类。
12. 相关模式
C o m p o s i t e模式可被用来实现宏命令。\r\nM e m e n t o模式可用来保持某个状态,命令用这一状态来取消它的效果。\r\n在被放入历史表列前必须被拷贝的命令起到一种原型的作用。