设计模式——设计模式选择——行为型

对象行为型模式

Chain Of Responsibility——职责链模式

意图

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

动机

考虑一个图形用户界面中的上下文有关的帮助机制。用户在界面的任一部分上点击就可以得到帮助信息,所提供的帮助依赖于点击的是界面的哪一部分以及其上下文。例如,对话框中的按钮的帮助信息就可能和主窗口中类似的按钮不同。如果对那一部分界面没有特定的帮助信息,那么帮助系统应该显示一个关于当前上下文的较一般的帮助信息一比如说,整个对话框。

因此很自然地,应根据普遍性(generality)即从最特殊到最普遍的顺序来组织帮助信息。而且,很明显,在这些用户界面对象中会有一个对象来处理帮助请求;至于是哪一个对象则取决于上下文以及可用的帮助具体到何种程度。

这儿的问题是提交帮助请求的对象(如按钮)并不明确知道谁是最终提供帮助的对象。我们要有一种办法将提交帮助请求的对象与可能提供帮助信息的对象解耦(decouple)。Chain of Responsibility模式告诉我们应该怎么做。

这一模式的想法是,给多个对象处理一个请求的机会,从而解耦发送者和接受者。该请求沿对象链传递直至其中一个对象处理它。

适用性

在以下条件下使用Responsibility链:

  • 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
  • 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • 可处理个请求的对象集合应被动态指定。

结构

设计模式——设计模式选择——行为型

参与者

  • Handler
    定义一个处理请求的接囗。
    实现后继链(可选)。
  • ConcreteHandler
    处理它所负责的请求。
    可访它的后继者。
    如果可处理该请求,就处理之;否则将该请求转发给它的后继者。
  • Client
    向链上的具体处理者(ConcreteHandler)对象提交请求。

协作

当客户提交一个请求时,请求沿链传递直至有一个ConcreteHandler对象负责处理它。

效果

Responsibility链有下列优点和缺点:

降低耦合度该

模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需知道该请求会被“正确”地处理。接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。

结果是,职责链可简化对象的相互连接。它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。

增强了给对象指派职责的灵活性

当在对象中分派职责时,职责链给你更多的灵活性。你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。

不保证被接受

既然一个请求没有明确的接收者,那么就不能保证它一定会被处理——该请求可能一直到链的末端都得不到处理。一个请求也可能因该链没有被正确配置而得不到处理。

实现

下面是在职责链模式中要考虑的实现问题:

实现后继者链

有两种方法可以实现后继者链。

  • 定义新的链接(通常在Handler中定义,但也可由ConcreteHandlers来定义
  • 使用已有的链接。

我们的例子中定义了新的链接,但你常常可使用已有的对象引用来形成后继者链。例如,在一个部分一整体层次结构中,父构件引用可定义一个部件的后继者。窗口组件(Widget)结构可能早已有这样的链接。Composite更详细地讨论了父构件引用。

当已有的链接能够支持你所需的链时,完全可以使用它们。这样你不需要明确定义链接,而且可以节省空间。但如果该结构不能反映应用所需的职责链,那么你必须定义额外的链接。

连接后继者

如果没有已有的引用可定义一个链,那么你必须自己引人它们。这种情况下Handler不仅定义该请求的接口,通常也维护后继链接。这样Handler就提供了HandleRequest的缺省实现:HandleRequest向后继者〔如果有的话)转发请求。如果ConcreteHandler子类对该请求不感兴趣,它不需重定义转发操作,因为它的缺省实现进行无

表示请求

可以有不同的方法表示请求。最简单的形式,比如在HandleHelp的例子中,请求是一个硬编码的(hard-coded)操作调用。这种形式方便而且安全,但你只能转发Handler类定义的固定的一组请求。

另一选择是使用一个处理函数,这个函数以一个请求码(如一个整型常数或一个字符串)为参数。这种方法支持请求数目不限。唯一的要求是发送方和接受方在请求如何编码问题上应达成一致。

这种方法更为灵活,但它需要用条件语句来区分请求代码以分派请求。另外,无法用类型安全的方法来传递请求参数,因此它们必须被手工打包和解包。显然,相对于直接调用一个操作来说它不太安全。

为解决参数传递间题,我们可使用独立的请求对象来封装请求参数。Request类可明确地描述请求,而新类型的请求可用它的子类来定义。这些子类可定义不同的请求参数。处理者必须知道请求的类型(即它们正使用哪一个Request子类)以访同这些参数。

为标识请求,Request可定义一个访问器(accessor)函数以返回该类的标识符。或者,如果实现语言支持的话,接受者可使用运行时的类型信息。

需要考虑增加内容

相关模式

职责链常与Composite一起使用。这种情况下,一个构件的父构件可作为它的后继。

Command(Action、Transaction)——命令模式

意图

将一个请求封装为一个对象.从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。

动机

有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输人。但工具箱不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。

命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的Command类,它定义了一个执行操作的接口。其最简单的形式是一个抽象的Execute操作。具体的Command子类将接收者作为其一个实例变量,并实现Execute操作指定接收者采取的动作。而接收者有执行该请求所需的具体信息。

适用性

当你有如下需求时,可使用Command模式:

  • 像上面讨论的Menultem对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。command模式是回调机制的一个面向对象的替代品。
  • 在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
  • 支持取消操作。Command的Excute操作可在实施操作前将状态存储起来。在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“取消”和“重做”。
  • 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读人记录下来的命令并用Execute操作重新执行它们。
  • 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务(transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。

结构

设计模式——设计模式选择——行为型

参与者

  • Command
    声明执行操作的接口。
  • ConcreteCOmmand
    将一个接收者对象绑定于一个动作。
    调用接收者相应的操作,以实现Execute。
  • Client
    创建一个具体命令对象并设定它的接收者。
  • Invoker
    要求该命令执行这个请求。
  • Receiver
    知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。

协作

  • Client创建一个Concretecommand对象并指定它的Receiver对象
  • 某Invoker对象存储该Concretecommand对象。
  • 该Invoker通过调用Command对象的Execute操作来提交一个请求。若该命令是可撤消的,Concretecommand就在执行Excute操作之前存储当前状态以用于取消该命令。
  • ConcreteCommand对象对调用它的Receiver的一些操作以执行该请求。

效果

Command模式有以下效果.

  1. Command模式将调用操作的对象与知道如何实现该操作的对象解耦。
  2. Command是头等的对象。它们可像其他的对象一样被操纵和扩展。
  3. 你可将多个命令装配成一个复合命令。例如是前面描述的MacroCommand类。一般说来,复合命令是Composite模式的一个实例。
  4. 增加新的Command很容易,因为这无需改变已有的类。

实现

实现Command模式时须考虑以下问题:

一个命令对象应达到何种智能程度

命令对象的能力可大可小。一个极端是它仅确定一个接收者和执行该请求的动作。另一极端是它自己实现所有功能,根本不需要额外的接收者对象。当需要定义与已有的类无关的命令,当没有合适的接收者,或当一个命令隐式地知道它的接收者时,可以使用后一极端方式。例如,创建另一个应用窗口的命令对象本身可能和任何其他的对象一样有能力创建该窗口。在这两个极端间的情况是命令对象有足够的信息可以动态的找到它们的接收者。

支持取消(undo)和重做(redo)

如果Command提供方法逆转(reverse)它们操作的执行(例如UnExecute或Undo操作),就可支持取消和重做功能。为达到这个目的,Concretecommand类可能需要存储额外的状态信息。这个状态包括:

  • 接收者对象,.它真正执行处理该请求的各操作。
  • 接收者上执行操作的参数。
  • 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。

若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的取消和重做,就需要有一个己被执行命令的历史表列,该表列的最大长度决定了取消和重做的级数。历史表列存储了已被执行的命令序列。向后遍历该表列并逆向执行命令是取消它们的结果;向前遍历并执行命令是重执行它们。

有时可能不得不将一个可撤销的命令在它可以被放人历史列表中之前先拷贝下来。这是因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间会发生变化.那就必须进行拷贝以区分相同命令的不同调用。

例如,一个删除选定对象的删除命令(DeleteCommand)在它每次被执行时,必须存储不同的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放人历史表列中。如果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放人历史表列中。在放人历史表列中之前必须被拷贝的那些Command起着原型(参见Prototype模式)的作用。

避免取消搡作过中的误积累

在实现一个可靠的、能保持原先语义的取消/重做机制时,可能会遇到滞后影响问题。由于命令重复的执行、取消执行,和重执行的过程可能会积累错误,以至一个应用的状态最终偏离初始值。这就有必要在Command中存人更多的信息以保证这些对象可被精确地复原成它们的初始状态。这里可使用Memento模式来让该Command访问这些信息而不暴露其他对象的内部信息。

使用c++模板(修改)

对(1)不能被取消〔2)不需要参数的命令,我们可使用c++模板来实现,这样可以避免为每一种动作和接收者都创建一个Command子类。

相关模式

Composite模式可被用来实现宏命令。
Memento模式可用来保持某个状态,命令用这一状态来取消它的效果。
在被放人历史表列前须被拷贝的命令起到一种原型的作用。

Interpreter——解释器模式

意图

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

动机

如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

例如,搜索匹配一个模式的字符串是一个常见问题。正则表达式是描述字符串模式的一种标准语言。与其为每一个的模式都构造一个特定的算法,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。

解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及
如何解释这些句子。

适用性

当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
- 该文法简单对于复杂的文法,文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式,这样可以节省空间而且还可能节省时间。
- 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下,转换器仍可用解释器模式实现,该模式仍是有用的。

结构

设计模式——设计模式选择——行为型

参与者

  • AbstractExpression
    声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。
  • TerminalExpression
    实现与文法中的终结符相关联的解释操作。
    一个句子中的每个终结符需要该类的一个实例。
  • NonterminalExpreSSion
    对文法中的每一条规则R::=R1 R2 … Rn 都需要一个NonterrmnalExpression类。
    为从R1到Rn的每个符号都维护一个AbstractExpression类型的实例变量。
    为文法中的非终结符实现解释(Interpret)操作。解释(Interpret)一般要递归地调用表示R1到Rn的那些对象的解释澡作。
  • Context
    包含解释器之外的一些全局信息。
  • Client
    构建(或被给定)表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语法树由NonterminalExpression和TerminalExpression的实例装配而成。
    调用解释操作。

协作

  • Client构建(或被给定)一个句子,它是NonterminalExpression和TerminalExpression的实例的一个抽象语法树.然后初始化上下文并调用解释操作。
  • 每一非终结符表达式节点定义相应子表达式的解释操作。而各终结符表达式的解释操作构成了递归的基础。
  • 每一节点的解释操作用上下文来存储和访问解释器的状态。

效果

解释器模式有下列的优点和不足:

易于改变和扩展文法

因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。已有的表达式可被增量式地改变,而新的表达式可定义为旧表达式的变体。

也易于实现文法

定义抽象语法树中各个节点的类的实现大体类似。这些类易于直接编写,通常它们也可用一个编译器或语法分析程序生成器自动生成。

复杂的文法难以维护

解释器模式为文法中的每一条规则至少定义了一个类(使用BNF定义的文法规则需要更多的类)。因此包含许多规则的文法可能难以管理和维护。可应用其他的设计模式来缓解这一问题。但当文法非常复杂时,其他的技术如语法分析程序或编译器生成器更为合适。

实现

Interpreter和Composite模式在实现上有许多相通的地方。下面所要考虑的一些特殊问题:

创建抽象语法树

解释器模式并未解释如何创建一个抽象的语法树。换言之,它不涉及语法分析。抽象语法树可用一个表驱动的语法分析程序来生成,也可用手写的(通常为递归下降法)语法分析程序创建,或直接由Client提供。

定义解释搡作

并不一定要在表达式类中定义解释操作。如果经常要创建一种新的解释器,那么使用Visitor模式将解释放人一个独立的“访问者”对象更好一些。例如,一个程序设计语言的会有许多在抽象语法树上的操作,比如类型检查、优化、代码生成,等等。恰当的做法是使用一个访问者以避免在每一个类上都定义这些操作。

与Flyweight模式共享终结符

在一些文法中,一个句子可能多次出现同一个终结符。此时最好共享那个符号的单个拷贝。计算机程序的文法是很好的例子——每个程序变量在整个代码中将会出现多次。

终结节点通常不存储关于它们在抽象语法树中位置的信息。在解释过程中,任何它们所需要的上下文信息都由父节点传递给它们。因此在共享的(内部的)状态和传人的(外部的)状态区分得很明确,这就用到了Flyweight模式。

相关模式

Composite模式:抽象语法树是一个复合模式的实例。
Flyweight模式:说明了如何在抽象语法树*享终结符。
Iterator:解释器可用一个迭代器遍历该结构。
Visitor:可用来在一个类中维护抽象语法树中的各节点的行为。

Iterator(Cursor)——迭代器模式

意图

提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。

动机

一个聚合对象,如列表(list),应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构,此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个表列上同时进行多个遍历。

迭代器模式都可帮你解决所有这些问题。这一糗式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放人一个迭代器(iterator)对象中。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素;即,它知道哪些元素己经遍历过了。

适用性

迭代器模式可用来:

  • 访问一个聚合对象的内容而无需暴露它的内部表示。
  • 支持对聚合对象的多种遍历。
  • 为遍历不同的聚合结构提供一个统一的接口(即,支持多态迭代)。

结构

设计模式——设计模式选择——行为型

参与者

  • Iterator
    代器定义访问和遍历元素的接口。
  • Concretelterator
    具体迭代器实现迭代器接口。
    对该聚合遍历时跟踪当前位置。
  • Aggregate
    聚合定义创建相应迭代器对象的接口。
  • ConcreteAggregate
    具体聚合实现创建相应迭代器的接口,该操作返回Concretelterator的一个适当的实例。

协作

Concretelterator跟踪聚合中的当前对象,并能够计算出待遍历的后继对象。

效果

迭代器模式有三个重要的作用:

它支持以不同的方式遍历一个聚合

复杂的聚合可用多种方式进行遍历。例如,代码生成和语义检查要遍历语法分析树。代码生成可以按中序或者按前序来遍历语法分析树。迭代器模式使得改变遍历算法变得很容易:仅需用一个不同的迭代器的实例代替原先的实例即可。你也可以自己定义迭代器的子类以支持新的遍历。

迭代器笸化了聚合的接口

有了迭代器的遍历接口,聚合本身就不再需要类似的遍历接口了。这样就简化了聚合的接口。

在同一个聚合上可以有多个遍历

每个迭代器保持它自己的遍历状态。因此你可以同时进行多个遍历

实现

迭代器在实现上有许多变化和选择。下面是一些较重要的实现。实现迭代器模式时常常
需要根据所使用的语言提供的控制结构来进行权衡。

谁控制该迭代

一个基本的问题是决定由哪一方来控制该迭代,是迭代器还是使用该迭代器的客户。当由客户来控制迭代时,该迭代器称为一个外部迭代器(external Iterator),面当由迭代器控制迭代时,该迭代器称为一个内部迭代器(internal Iterator)a。使用外部迭代器的客户必须主动推进遍历的步伐,显式地向迭代器请求下一个元素。相反地,若使用内部迭代器,客户只需向其提交一个待执行的操作,而迭代器将对聚合中的每一个元素实施该操作。

外部迭代器比内部迭代器更灵活。例如,若要比较两个集合是否相等,这个功能很容易用外部迭代器实现,而几乎无法用内部迭代器实现。

谁定义遍历算法

迭代器不是唯一可定义遍历算法的地方。聚合本身也可以定义遍历算法,并在遍历过程中用迭代器来存储当前迭代的状态。我们称这种迭代器为一个游标(cursor),因为它仅用来指示当前位置。客户会以这个游标为一个参数调用该聚合的Next操作,
而Next操作将改变这个指示器的状态。。

如果迭代器负责遍历算法,那么将易于在相同的聚合上使用不同的迭代算法,同时也易于在不同的聚合上重用相同的算法。从另一方面说,遍历算法可能需要访问聚合的私有变量。如果这样,将遍历算法放人迭代器中会破坏聚合的封装性。

迭代器健壮程度如何

在遍历一个聚合的同时更改这个聚合可能是危险的。如果在遍历聚合的时候增加或删除该聚合元素,可能会导致两次访问同一个元素或者遗漏掉某个元素一个简单的解决办法是拷贝该聚合,并对该拷贝实施遍历,但一般来说这样做代价太高。

一个健壮的迭代器(robust Iterator)保证插人和删除操作不会干扰遍历,且不需拷贝该聚合。有许多方法来实现健壮的迭代器。其中大多数需要向这个聚合注册该迭代器。当插人或删除元素时,该聚合要么调整迭代器的内部状态,要么在内部的维护额外的信息以保证正确的遍历。

附加的迭代器操作

迭代器的最小接口由First、Next、IsDone和Currentltem操作组成。其他一些操作可能也很有用。例如,对有序的聚合可用一个Previous操作将迭代器定位到前一个元素。

在C++中使用多态的迭代器(?)

使用多态迭代器是有代价的。它们要求用一个Factory
Method动态的分配迭代器对象。因此仅当必须多态时才使用它们。否则使用在栈中分配内存
的具体的迭代器。
多态迭代器有另一个缺点:客户必须负责删除它们。这容易导致错误,因为你容易忘记释
放一个使用堆分配的迭代器对象,当一个操作有多个出口时尤其如此。而且其间如果有异常
被触发的话,迭代器对象将永远不会被释放。
Proxy模式提供了一个补救方法。我们可使用一个栈分配的proxy作为实际迭代器的中间代理。该代理在其析构器中删除该迭代器。这样当该代理生命周期结束时,实际迭代器将同它一起被释放。即使是在发生异常时,该代理机制能保证正确地清除迭代器对象。

迭代器可有特权访问

迭代器可被看为创建它的聚合的一个扩展。迭代器和聚合紧密耦合。在c++中我们可让迭代器作为它的聚合的一个友元(friend)来表示这种紧密的关系。这样你就不需要在聚合类中定义一些仅为迭代器所使用的操作。

但是,这样的特权访问可能使定义新的遍历变得很难,因为它将要求改变该聚合的接口增加另一个友元。为避免这一问题,迭代器类可包含一些protected操作来访问聚合类的重要的非公共可见的成员。迭代器子类(且只有迭代器子类)可使用这些protected操作来得到对该聚合的特权访问。

用于复合对象的迭代器

在Composite模式中的那些递归聚合结构上,外部迭代器可能难以实现,因为在该结构中不同对象处于嵌套聚合的多个不同层次,因此一个外部迭代器为跟踪当前的对象必须存储一条纵贯该Composite的路径。有时使用一个内部迭代器会更容
易一些。它仅需递归地调用自己即可,这样就隐式地将路径存储在调用栈中,而无需显式地维护当前对象位置。

如果复合中的节点有一个接口可以从一个节点移到它的兄弟节点、父节点和子节点.那么基于游标的迭代器是个更好的选择。游标只需跟踪当前的节点;它可依赖这种节点接口来遍历该复合对象。

复合常常需要用多种方法遍历。前序,后序,中序以及广度优先遍历都是常用的。你可用不同的迭代器类来支持不同的遍历。

空迭代器

一个空迭代器(Null Iterator)是一个退化的迭代器,它有助于处理边界条件。根据定义,一个Null lterator总是已经完成了遍历:即,它的IsDone操作总是返回true。空迭代器使得更容易遍历树形结构的聚合(如复合对象)。在遍历过程中的每一节点,都可向当前的元素请求遍历其各个子结点的迭代器。该聚合元素将返回一个具体的迭代器。但叶节点元素返回Null Iterator的一个实例。这就使我们可以用一种统一的方式实现在整个结构上的遍历。

相关模式

Composite:迭代器常被应用到象复合这样的递归结构上。
Factory Method:多态迭代器靠FactoryMethod来例化适当的迭代器子类。
Memento:常与迭代器模式一起使用。迭代器可使用一个memento来捕获一个迭代的状态。迭代器在其内部存储memento。

Mediator——中介者模式

意图

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从
而使其耦合松散,而且可以独立地改变它们之间的交互。

动机

面向对象设计鼓励将行为分布到各个对象中。这种分布可能会导致对象间有许多连接。在最坏的情况下,每一个对象都知道其他所有对象。

虽然将一个系统分割成许多对象通常可以增强可复用性,但是对象间相互连接的激增又会降低其可复用性。大量的相互连接使得一个对象似乎不太可能在没有其他对象的支持下工作一一系统表现为一个不可分割的整体。而且,对系统的行为进行任何较大的改动都十分困难,因为行为被分布在许多对象中。结果是,你可能不得不定义很多子类以定制系统的行为。

适用性

在下列情况下使用中介者模式:

  • 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理
  • 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
  • 想定制一个分布在多个类中的行为,而又不想生成太多的子类。

结构

设计模式——设计模式选择——行为型

参与者

  • Mediator
    中介者定义一个接口用于与各同事〔colleague〕对象通信。
  • ConcreteMediator
    具体中介者通过协调各同事对象实现协作行为。
    了解并维护它的各个同事。
  • Colleagueclass
    每一个同事类都知道它的中介者对象。
    每一个同事对象在需与其他的同事通信的时候,与它的中介者通信。

协作

同事向一个中介者对象发送和接收请求。中介者在各同事间适当地转发请求以实现协作行为。

效果

中介者模式有以下优点和缺点:

减少了子类生成

Mediator将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成Mediator的子类即可。这样各个Colleague类可被重用。

它将各Colleague解耦

Mediator有利于各Colleague间的松耦合.你可以独立的改变和复用各Colleague类和Mediator类。

它简化了对象协议

用Mediator和各Colleague间的一对多的交互来代替多对多的交互。一对多的关系更易于理解、维护和扩展。

它对对象如何作进行了抽象

将中介作为一个独立的概念并将其封装在一个对象中,使你将注意力从对象各自本身的行为转移到它们之间的交互上来。这有助于弄清楚一个系统中的对象是如何交互的。

它使控制集中化

中介者模式将交互的复杂性变为中介者的复杂性。因为中介者封装了协议,它可能变得比任一个coIleague都复杂。这可能使得中介者自身成为一个难于维护的庞然大物。

实现

下面是与中介者模式有关的一些实现问题.

忽咯抽象的Mediator类

当各Colleague仅与一个Mediator—起工作时,没有必要定义个抽象的Mediator类。Mediator类提供的抽象耦合已经使各Colleague可与不同的Mediator子类一起工作,反之亦然。

Colleague—Mediator通信

当一个感兴趣的事件发生时,Colleague必须与其Mediator通信。一种实现方法是使用Observer模式,将Mediator实现为一个Observer,各Colleague作为Subject,一旦其状态改变就发送通知给Mediator。Mediator作出的响应是将状态改变的结果传播给其他的Colleague。

另一个方法是在Mediator中定义一个特殊的通知接口,各Colleague在通信时直接调用该接口。

相关模式

Facade与中介者的不同之处在于它是对一个对象子系统进行抽象,从而提供了一个更为方便的接口。它的协议是单向的,即Facade对象对这个子系统类提出请求,但反之则不行。相反,Mediator提供了各Colleague对象不支持或不能支持的协作行为,而且协议是多向的。

Colleague可使用Observer模式与Mediator通信。

Memento(Token)——备忘录模式

意图

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

动机

有时有必要记录一个对象的内部状态。为了允许用户取消不确定的擬作或从错误中恢复过来,需要实现检查点和取消机制,而要实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息,使得其状态不能被其他对象访问,也就不可能在该对象之外保存其状态。而暴露其内部状态又将违反封装的原则,可能有损应用的可靠性和可扩展性。

适用性

在以下情况下使用备忘录模式:

  • 必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态
  • 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。

结构

设计模式——设计模式选择——行为型

参与者

  • Memento
    备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态。
    防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口,管理者(caretaker)只能看到备忘录的窄接口一一它只能将备忘录传递给其他对象。相反,原发器能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。
  • Originator
    原发器创建一个备忘录,用以记录当前时刻它的内部状态。
    使用备忘录恢复内部状态,。
  • Caretaker
    负责保存好备忘录。
    不能对备忘录的内容进行操作或检查。

协作

  • 管理器向原发器请求一个备忘录,保留一段时间后,将其送回给原发器,如下面的交互图所示。
    设计模式——设计模式选择——行为型

有时管理者不会将备忘录返回给原发器,因为原发器可能根本不需要退到先前的状态。

  • 备忘录是被动的。只有创建备忘录的原发器会对它的状态进行赋值和检索。

效果

备忘录模式有以下一些效果:

保持封装边界

使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内部信息对其他对象屏蔽起来,从而保持了封装边界。

它简化了原发器

在其他的保持封装性的设计中,Originator负责保持客户请求过的内部状态版本。这就把所有存储管理的重任交给了Originator。让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。

使用备忘录可能代价很高

如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复Onginator状态的开销不大,否则该模式可能并不合适。参见实现一节中关于增量式改变的讨论。

定义窄接口和宽接口

在一些语言中可能难以保证只有原发器可访问备忘录的状态。

维护备忘录的潜在代价

管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生人量的存储

实现

下面是当实现备忘录模式时应考虑的两个问题:

语言支持

备忘录有两个接口:一个为原发器所使用的宽接口,一个为其他对象所使用的窄接口。理想的实现语言应可支持两级的静态保护。在C++中,可将Originator作为Memento的一个友元,并使Memento宽接口为私有的。只有窄接口应该被声明为公共的。

存储增量式改变

如果备忘录的创建及其返回(给它们的原发器)的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。

例如,一个包含可撤消的命令的历史列表可使用备忘录以保证当命令被取消时,它们可以被恢复到正确的状态(参见command)。历史列表定义了一个特定的顺序,按照这个顺序命令可以被取消和重做。这意味着备忘录可以只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。在前面动机一节给出的例子中,约束解释器可以仅存储那变化了的内部结构,以保持直线与矩形相连,而不是存储这些对象的绝对位置。

相关模式

Command:命令可使用备忘录来为可撤消的操作维护状态。
Iterator:如前所述备忘录可用于迭代.

Observer(Dependents,Publish-Subscribe)观察者模式

意图

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

动机

将一个系统分复成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。

适用性

在以下任一情况下可以使用观察者模式:

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  • 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
  • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象是紧密耦合的。

结构

设计模式——设计模式选择——行为型

参与者

  • Subject
    目标知道它的观察者。可以有任意多个观察者观察同一个目标。
    提供注册和删除观察者对象的接口。
  • Observer
    为那些在目标发生改变时需获得通知的对象定义一个更新接口。
  • ConcreteSubject
    将有关状态存入各ConcreteObserver对象。
    当它的状态发生改变时,向它的各个观察者发出通知。
  • ConcreteObserver
    维护一个指向ConcreteSubject对象的引用。
    存储有关状态,这些状态应与目标的状态保持一致。
    实现Observer的更新接口以使自身状态与目标的状态保持一致。

协作

  • 当ConcreteSubject发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。
  • 在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息。ConcreteSubject使用这些信息以使它的状态与目标对象的状态一致。
    下面的交互图说明了一个目标对象和两个观察者之间的协作.
    设计模式——设计模式选择——行为型

注意发出改变请求的Observer对象并不立即更新,而是将其推迟到它从目标得到一个通知之后。Notify不总是由目标对象调用。它也可被一个观察者或其它对象调用。

效果

Observer模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用
其观察者,反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。
下面是观察者模式其它一些优缺点

目标和观察者间的轴象耦合

一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。

因为目标和观察者不是紧密耦合的,它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性)要么必须放在这两层的某一层中(这可能会损害层次抽象)。

支持广播通信

不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有己向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的*。处理还是忽略一个通知取决于观察者。

意外的更新

因为一个观察者并不知道其它观察者的存在.它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。

简单的更新协议不提供具体细节说明目标中什么被改变了,这就使得上述问题更加严重。如果没有其他协议帮助观察者发现什么发生了改变,它们可能会*尽力减少改变。

实现

这一节讨论一些与实现依赖机制相关的问题。

创建目标到其观察者之间的映射

一个目标对象跟踪它应通知的观察者的最简单的方法是显式地在目标中保存对它们的引用。然而,当目标很多而观察者较少时,这样存储可能代价太高。一个解决办法是用时间换空间,用一个关联查找机制(例如一个hash表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这一方法增加了访问观察者的开销。

观察多个目标

在某些情况下,一个观察者依赖于多个目标可能是有意义的。例如,个表格对象可能依赖于多个数据源。在这种情况下,老须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的一个参数,让观察者知道应去检查哪一个目标。

谁触发更新

目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用Notify来触发更新?此时有两个选择:

  • 由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低
  • 让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式较易出错。

对已删除目标的悬挂引用

删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是,当一个目标被除时,让它通知它的观察者将对该目标的引用复位。一般来说,不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。

在发出通知前确保目标的态自身是一致的

在发出通知前确保目标的态自身是一致这一点很重要,因为观察者在更新其状态的过程中需要查询目标的当前状态。

当Subject的子类调用继承的该项操作时,很容易无意中违反这条自身一致的准则。

你可以用抽象的Subject类中的模板方法(Template Method)发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义了Subject的操作时,还可以保证该对象的状态是自身一致的。

顺便提一句,在文档中记录是哪一个Subject操作触发通知总是应该的。

避免特定于观察者的更新协议一一推/拉模型

观察者模式的实觋经常需要让目标广播关于其改变的其他一些信息。目标将这些信息作为Update操作一个参数传递出去。这些信息的量可能很小,也可能很大。

一个极端情况是,目标向观察者发送关于改变的详细信息,而不管它们需要与否。我们称之为推模型(push model)。另一个极端是拉模型(puIl model);目标除最小通知外什么也不送出,而在此之后由观察者显式地向目标询问细节。

拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。另一方面。拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。

显式地指定感兴趣的改变

你可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时,目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法一种途径是,对使用目标对象的方面(aspects)的概念。

封装复杂的更新语义

当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器(ChangeManager)。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如,如果一个操作涉及到对几个相互依赖的目标进行改动,就必须保证仅在所有的目标都己更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。
ChangeManager有三个责任.

  1. 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用,反之亦然。
  2. 它定义一个特定的更新策略。
  3. 根据一个目标的请求,它更新所有依赖于这个目标的观察者。

下页的框图描述了一个简单的基于ChangeManager的Observer模式的实现。有两种特殊的ChangeManager。SimpleChangeManager总是更新每一个目标的所有观察者,比较简单。相反,DAGChangeManager处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时,DAGChangeManager要比SimpleChangeManager更好一些。在这种情况下,两个或更多个目标中产生的改变可能会产生冗余的更新。DAGChangeManager保证观察者仅接收一个更新。当然,当不存在多重更新的问题时,SimpleChangeManager更好一些。
ChangeManager是一个Mediator模式的实例。通常只有一个ChangeManager。并且它是全局可见的。这里Singleton模式可能有用。

结合目标类和观察者类

用不支持多重继承的语言(如Smallta]k)书写的类库通常不单独定义Subject和Observer类,而是将它们的接口结合到一个类中。这就允许你定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。例如在Smalltalk中,subject和Observer接口定义于根类Object中,使得它们所有的类都可用。

相关模式

Mediator:通过封装复杂的更新语义,ChangeManager充当目标和观察者之间的中介者。
Singleton:ChangeManager可使用Singletion模式来保证它是唯一的并且是可全局访问的。

STATE(Objects for States)——状态模式

意图

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类

动机

考虑一个表示网络连接的类TCPConnection。一个TCPConnection对象的状态处于若干不
同状态之一:连接已建立(Established)、正在监听(Listening)、连接已关闭(Closed)。当一个
TCPConnection对象收到其他对象的请求时,它根据自身的当前状态作出不同的反应。

适用性

在下面的两种情况下均可使用State模式.

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
  • 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这一相同的条件结构。State模式将每一个条件分支放人一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

结构

设计模式——设计模式选择——行为型

参与者

  • Context
    定义客户感兴趣的接口。
    维护一个ConcreteState子类的实例.这个实例定义当前状态。
  • State
    定义一个接口以封装与Context的一个特定状态相关的行为。
  • ConcreteState subclasses
    每一子类实现一个与Context的一个状态相关的行为。

协作

  • Context将与状态相关的请求委托给当前的ConcreteState对象处理。
  • Context可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要
    时可访问Context。
  • Context是客户使用的主要接口。客户可用状态对象来配置一个Context,一旦一个Context配置完毕,它的客户不冉需要直接与状态对象打交道。
  • Context配置完毕,它的客户不再需要直接与状态对象打交道。
  • Context或ConcreteState子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条件下进行状态转换。

效果

state模式有下面一些效果:

它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来

State模式将所有与一个特定的状态相关的行为都放人一个对象中。因为所有与状态相关的代码都存在于某一个State子类中,所以通过定义新的子类可以很容易的增加新的状态和转换。

另一个方法是使用数据值定义内部状态并且让Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件语句或case语句。增加一个新的状态可能需要改变若干个操作,这就使得维护变得复杂了。

State模式避免了这个问题,但可能会引人另一个问题,因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些,否则需要使用巨大的条件语句。

正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中,而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。

它使得状态转换显式化

当一个对象仅以内部数据值来定义当前状态时,其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引人独立的对象使得转换变得更加明确。而且,State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的——只需新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值。

State对象可被共享

如果State对象没有实例变量——即它们表示的状态完全以它们的类型来编码——那么各Context对象可以共享一个State对象。当状态以这种方式被共享时,它们必然是没有内部状态,只有行为的轻量级对象(参见Flyweight)。

实现

实现State模式有多方面的考虑:

谁定义状态转换

State模式不指定哪一个参与者定义状态转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然而若让State子类自身指定它们的后继状态以及何时进行转换,通常更灵活更合适。这需要Context增加一个接口,让State对象显式地设定Context的当前状态。

用这种方法分散转换逻辑可以很容易地定义新的&ate子类来修改和扩展该逻辑。这样做的一个缺点是,一个State子类至少拥有一个其他子类的信息,这就再各子类之间产生了实现依赖。

基于表的另一种方法

另一种将结构加载在状态驱动的代码上的方法:他使用表将输人映射到状态转换。对每一个状态,一张表将每一个可能的输人映射到一个后继状态。实际上,这种方法将条件代码(和State模式下的虚函数)映射为一个查找表。

表的主要好处是它们的规则性:你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而它也有一些缺点:

  • 对表的查找通常不如(虚)函数调用效率高。
  • 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。
  • 通常难以加入伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。

表驱动的状态机和State模式的主要区别可以被总结如下:State模式对与状态相关的行为进行建模,而表驱动的方法着重于定义状态转换。

创建和销毁State对象

一个常见的值得考虑的实现上的权衡是,究竟是(1)仅当需要state对象时才创建它们并随后销毁它们,还是(2)提前创建它们并且始终不销毁它们。当将要进人的状态在运行时是不可知的,并且上下文不经常改变状态时,第一种选择较为可取。这种方法避免创建不会被用到的对象,如果state对象存储大量的信息时这一点很重要。当状态改变很频繁时,第二种方法较好。在这种情况下最好避免销毁状态,因为可能很快再次需要用到它们。此时可以预先一次付清创建各个状态对象的开销,并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方便,因为Context必须保存对所有可能会进入的那些状态的引用。

使用动态继承

改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的办法实现,但这在大多数面向对象程序设计语言中都是不可能的。有一些基于委托的语言却是例外,它们提供这种机制,从而直接支持State模式。它们中的对象可将操作委托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效地改变了继承的结构。这一机制允许对象改变它们的行为,也就是改变它们的类。

相关模式

Flyweight模式解释了何时以及怎样共享状态对象。
状态对象通常是Singletion。

Strategy(Policy)——策略模式

意图

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独
立于使用它的客户而变化。

动机

有许多算法可对一个正文流进行分行。将这些算法硬编进使用它们的类中是不可取的,其原因如下:

  • 需要换行功能的客户程序如果直接包含换行算法代码的话将会变得复杂,这使得客户程序庞大并且难以维护、尤其当其需要支持多种换行算法时问题会更加严重。
  • 不同的时候需要不同的算法,我们不想支持我们并不使用的换行算法。
  • 当换行功能是客户程序的一个难以分割的成分时,增加新的换行算法或改变现有算法将
    十分困难。

我们可以定义一些类来封装不同的换行算法,从而避免这些问题。一个以这种方法封装的算法称为一个策略(Strategy)。

适用性

当存在以下情况时使用Strategy模式

  • 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
  • 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时,可以使用策略模式
  • 算法使用客户不应该知道的数据。可使策略模式以避免暴露复杂的、与算法相关的数据结构。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移人它们各自的strategy类中以代替这些条件语句。

结构

设计模式——设计模式选择——行为型

参与者

  • Strategy
    定义所有支持的算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法。
  • ConcreteStrategy
    以strategy接口实现某具体算法。
  • Context
    用一个ConcreteStrategy对象来配置。
    维护一个对Strategy对象的引用。
    可定义一个接口来让Strategy访问它的数据。

协作

  • Strategy和Context相互作用以实现选定的算法。当算法被调用时,Context可以将该算法所需要的所有数据都传递给该Strategy。或者,Context可以将自身作为一个参数传递给Strategy操作。这就让Strategy在需要时可以回调Context对。
  • Context将它的客户的请求转发给它的Strategy。客户通常创建并传递一个ConcreteStrategy对象给该Context;这样,客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户从中选择。

效果

Strategy模式有下面的一些优点和缺点:

相关算法系列

Strategy类层次Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。

一个替代继承的方法

继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到Context中,而将算法的实现与Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类,它们之间的唯一差别是它们所使用的算法或行为。将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。

消除了一些条件语句

Strategy模式提供了用条件语句选择所需的行为以外的另一种选
择。当不同的行为堆砌在一个类中时,很难免使用条件语句来选择合适的行为。将行为封装
在一个个独立的Strategy类中消除了这些条件语句。

实现的选择

Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同策略中进行选择。

客户必须了解不同的Strategy

本模式有一个潜在的缺点,就是一个客户要选择一个合适的strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时,才需要使用Strategy模式。

Strategy和Context之间的通信开销

无论各个ConcreteStrategy实现的算法是简单还是复杂,它们都共享Strategy定义的接口。因此很可能某些ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样同题,那么将需要在Strategy和Context之间更进行紧密的耦合。

增加了对象的数目

Strategy增加了一个应用中的对象的数目。有时你可以将Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由Context维护。Context在每一次对Strategy对象的请求中都将这个状态传递过去。共享的Stragey不应在各次调用之间维护状态。Flyweight模式更详细地描述了这一方法。

实现

考虑下面的实现问题:

定义Strategy和Context接口

Strategy和context接口必须使得concretestrategy能够有效的访问它所需要的Context中的任何数据,反之亦然。一种办法是让context将数据放在参数中传递给Strategy操作一一也就是说,将数据发送给Strategye这使得Strategy和Context解耦。但另一方面,Context可能发送一些Strategy不需要的数据。

另一种办法是让Context将自身作为一个参数传递给Strategy,该Strategy再显式地向该Context请求数据。或者,Strategy可以存储对它的context的一个引用,这样根本不再需要传递任何东西。这两种情况下,Strategy都可以请求到它所需要的数据。但现在Context必须对它的数据定义一个更为精细的接口,这将Strategy和context更紧密地耦合在一起。

将Strategy作为模板参数

在c++中,可利用模板机制用一个Strategy来配置一个类。然而这种技术仅当下面条件满足时才可以使用(1)可以在编译时选择Strategy(2)它不需在运行时改变。在这种情况下,要被配置的类(如,Context)被定义为以一个Strategy类作为一个参数的模板类。

使用模板不再需要定义给Strategy定义接口的抽象类。把Strategy作为一个模板参数也使
得可以将一个Strategy和它的Context静态地绑定在一起,从而提高效率。

使strategy对象成为可选的

如果即使在不使用额外的Strategy对象的情况下,Context也还有意义的话,那么它还可以被简化。Context在访问某strategy前先检查它是否存在,如果有,那么就使用它;如果没有,那么Context执行缺省的行为。这种方法的好处是客户根本不需要处理Strategy对象,除非它们不喜欢缺省的行为。

相关模式

flyweight:strategy对象经常是很好的轻量级对象

Template Method——模板方法

意图

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

动机

考虑一个提供AppllCation和Document类的应用框架。Application类负责打开一个已有的以外部形式存储的文档,如一个文件。一旦一个文档中的信息从该文件中读出后,它就由一个Document对象表示。
用框架构建的应用可以通过继承Apphcation和Document来满足特定的需求。例如,一个绘图应用定义DrawAppIication和DrawDocument子类;一个电子表格应用定义SpreadsheetApplication和Spreadsheetl)ocument子类。

适用性

模板方法应用于下列情况:

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 控制子类扩展。模板方法只在特定点调用”hook”操作,这样就只允许在这些点进行扩展。

结构

设计模式——设计模式选择——行为型

参与者

  • AbstractClass
    定义抽象的原语操作(primitive operauon),具体的子类将重定义它们以实现一个算法的各步骤。
    实现一个模板方法,定义一个算法的骨架。该模板方法不仅调用原语操作,也调用定义在AbstractC1ass或其他对象中的操作。
  • ConcreteClass
    实现原语操作以完成算法中与特定子类相关的步骤。

写作

ConcreteCIass靠AbstractCIass来实现算法中不变的步骤。

效果

模板方法是一种代码复用的基本技术。它们在类库中尤为重要,它们提取了类库中的公共行为。
模板方法导致一种反向的控制结构,这种结构有时被称为“好莱坞法则”,即“别找我们,我们找你”。这指的是一个父类调用一个子类的操作,而不是相反。
模板方法调用下列类型的操作:

  • 具体的操作(ConcreteClass或对客户类的操作)。
  • 具体的AbstractCIass的操作(即,通常对子类有用的操作)。
  • 原语操作(即,抽象操作)。
  • Factory Method。
  • 钩子操作(hook operations),它提供了缺省的行为,子类可以在必要时进行扩展。一个钩子操作在缺省操作通常是一个空操作。

实现

有三个实现问题值得注意:

使用C++访问控制

在C++中,一个模板方法调用的原语操作可以被定义为保护成员。这保证它们只被模板方法调用。必须重定义的原语操作须定义为纯虚函数。模板方法自身不需被重定义;因此可以将模板方法定义为一个非虚成员函数。

尽量减少原语操作定义

模板方法的一个重要目的是尽量减少一个子类具体实现该算法时必须重定义的那些原语操作的数目。需要重定义的操作越多,客户程序就越冗长。

命名约定

可以给应被重定义的那些操作的名字加上一个前缀以识别它们。

相关模式

Factory Method模式常被模板方法调用。
Strategy:模板方法使用继承来改变算法的一部分。Strategy使用委托来改变整个算法。

Vistor——访问者模式

意图

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提
下定义作用于这些元素的新操作。

动机

考虑一个编译器,它将源程序表示为一个抽象语法树。该编译器需在抽象语法树上实施某些操作以进行“静态语义”分析,例如检查是否所有的变量都已经被定义了。它也需要生成代码。因此它可能要定义许多操作以进行类型检查、代码优化、流程分析,检查变量是否在使用前被赋初值,等等。此外,还可使用抽象语法树进行优美格式打印、程序重构、codei
nstrumentation以及对程序进行多种度量。

这些操作大多要求对不同的节点进行不同的处理。例如对代表赋值语句的结点的处理就不同于对代表变量或算术表达式的结点的处理。因此有用于赋值语句的类,有用于变量访问的类,还有用于算术表达式的类,等等。结点类的集合当然依赖于被编译的语言,但对于一个给定的语言其变化不大。

适用性

在下列情况下使用Visitor模式;

  • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
  • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。

结构

设计模式——设计模式选择——行为型

参与者

  • Visitor
    为该对象结构中ConcreteElement的每一个类声明一个Visit操作。该操作的名字和特征标识了发请求给该访问者的那个类。这使得访问者可以确定正被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
  • ConcreteVisitor
    实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片断乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果。
  • Element
    定义一个Accept操作,它以一个访问者为参数
  • ConcreteElement
    实现Accept操作,该操作以一个访问者为参数
  • ObjectStructure
    能枚举它的元素
    可以提供一个高层的接口以允许该访问者访问它的元素。
    可以是一个复合(参见Composite)或是一个集合,如一个列表或一个无序集合。

协作

  • 一个使用Visitor模式的客户必须创建一个ConcreteVisitor对象,然后遍历该对象结构,并用该访问者访问每一个元素。
  • 当一个元素被访问时,它调用对应于它的类的Visitor操作。如果必要,该元素将自身作为这个操作的一个参数以便该访问者访问它的状态。
    下面的交互框图说明了一个对象结构、一个访问者和两个元素之间的协作
    设计模式——设计模式选择——行为型

效果

下面是访问者模式的一些优缺点:

访问者模式使得易于增加新的操作

访问者使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。

访问者集中相关的操作而分离无关的操作

相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这就既简化了这些元素的类,也简化了在这些访问者中定义的算法c所有与它的算法相关的数据结构都可以被隐藏在访问者中。

增加新的ConcreteElement类很困难

Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteEIement都要在Vistor中添加一个新的抽象操作,并在每一个ConcretVisitor类中实现相应的操作。有时可以在Visitor中提供一个缺省的实现,这一实现可以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。

所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象结构上的算法呢还是构成该结构的各个对象的类。如果老是有新的ConcretElement类加人进来的话,Vistor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一些。如果Element类层次是稳定的,而你不断地增加操作获修改算法,访问者模式可以帮助你管理这些改动。

通过类层次进行访问

一个迭代器可以通过调用节点对象的特定操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。例如,定义在第5章的Iterator接口只能访问类型为Item的对象:

这就意味着所有该迭代器能够访问的元素都有一个共同的父类Item。

访问者没有这种限制。它可以访问不具有相同父类的对象。可以对一个Visitor接口增加任何类型的对象。

累积状态

当访问者访问对象结构中的每一个元素时,它可能会累积状态。如果没有访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。

破坏封装

访问者方法假定ConcreteElement接口的功能足够强,足以让访问者进行它们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它的封装性。

实现

每一个对象结构将有一个相关的Visitor类。这个抽象的访问者类为定义对象结构的每一个ConcreteEIement类声明一个VisitConcreteElement操作。每一个Visitor上的Visitk作声明它的参数为一个特定的ConcreteEIement,以允许该Visitor直接访问ConcreteEIement的接口。ConcreteVistor类重定义每一个Visit操作,从而为相应的ConcreteElement类实现与特定访问者相关的行为。

每个ConcreteE1ement类实现一个Accept操作,这个操作调用访问者中相应于本ConcreteE1ement类的Visit…的操作。这样最终得到调用的操作不仅依赖于该元素的类也依赖于访问者的类。

下面是当应用Visitor模式时产生的其他两个实现问题:

双分派(Double-dispatch)

访问者模式允许你不改变类即可有效地增加其上的操作。为达到这一效果使用了一种称为双分派(double-dispatch)的技术。这是一种很著名的技术。事实上,一些编程语言甚至直接支持这一技术。而象c++和Smalltalk这样的语言支持单分派(single-dispatch)。

在单分派语言中,到底由哪一种操作将来实现一个请求取决于两个方面:该请求的名和接收者的类型。

双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。Accept是一个double-dispatch操作。它的含义决定于两个类型:Visitor的类型和Element的类型。双分派使得访问者可以对每一个类元的素请求不同的操作。

这是Visitor模式的关键所在:得到执行的操作不仅决定于Visitor的类型还决定于它访问的Element的类型。可以不将操作静态地绑定在Element接口中,而将其安放在一个Visitor中,并使用Accept在运行时进行绑定。扩展Element接口就等于定义一个新的Visitor子类而不是多个新的Element子类。

谁负责遍历对象结构

一个访问者必须访问这个对象结构的每一个元素。问题是,它怎样做?我们可以将遍历的责任放到下面三个地方中的任意一个:对象结构中,访问者中,或一个独立的迭代器对象中(参见Iterator)。

通常由对象结构负责迭代。一个集合只需对它的元素进行迭代,并对每一个元素调用Accept操作。而一个复合通常让Accept操作遍历该元素的各子构件并对它们中的每一个递归地调用Accept。

另一个解决方案是使用一个迭代器来访问各个元素。既可以使用内部迭代器也可以使用外部迭代器,到底用哪一个取决于哪一个可用和哪一个最有效。因为内部迭代器由对象结构实现,使用一个内部迭代器很大程度上就像是让对象结构负责送代。主要区别在于一个内部迭代器不会产生双分派一一它将以该元素为一个参数调用访问者的一个操作而不是以访问者为参数调用元素的一个操作。不过,如果访问者的操作仅简单地调用该元素的操作而无需递归的话,使用一个内部迭代器的Visitor模式很容易使用。

甚至可以将遍历算法放在访问者中,尽管这样将导致对每一个聚合Concrete Element,在每一个Concrete Visitor中都要复制遍历的代码。将该遍历策略放在访问者中的主要原因是想实现一个特别复杂的遍历,它依赖于对该对象结构的操作结果。

相关模式

Composite:访问者可以用于对一个由Composite模式定义的对象结构进行操作。
Interpreter:访问者可以用于解释。

行为模式的讨论

封裝变化

封装变化是很多行为模式的主题。当一个程序的某个方面的特征经常发生改变时,这些
模式就定义一个封装这个方面的对象。这样当该程序的其他部分依赖于这个方面时,它们都
可以与此对象协作。这些模式通常定义一个抽象类来描述这些封装变化的对象,并且通常该
模式依据这个对象来命名。例如,

  • 一个strategy对象封装一个算法(Strategy)。
  • 一个state对象封装一个与状态相关的行为(State)。
  • 一个Mediator对象封装对象间的协议(Meditator)。
  • 一个Iterator对象封装访问和遍历一个聚集对象中的各个构件的方法(Iterator)。

这些模式描述了程序中很可能会改变的方面。大多数模式有两种对象:封装该方面特征的新对象,和使用这些新的对象的已有对象。如果不使用这些模式的话,通常这些新对象的功能就会变成这些已有对象的难以分割的一部分。例如,一个Strategy的代码可能会被嵌人到其Context类中,而一个state的代码可能会在该状态的Context类中直接实现。

但不是所有的对象行为模式都象这样分割功能。例如,Chain of Responsibility可以处理任意数目的对象(即一个链),而所有这些对象可能已经存在于系统中了。

职责链说明了行为模式间的另一个不同点:并非所有的行为模式都定义类之间的静态通信关系。职责链提供在数目可变的对象间进行通信的机制。其他模式涉及到一些作为参数传递的对象。

对象作为参数

一些模式引人总是被用作参数的对象。例如Visitor。一个Visitor对象是一个多态的Accept操作的参数,这个操作作用于该Visitor对象访问的对象。虽然以前通常代替Visitor模式的方法是将Visitor代码分布在一些对象结构的类中,但visitor从来都不是它所访问的对象的一部分。

其他模式定义一些可作为令牌到处传递的对象,这些对象将在稍后被调用。Command和Memento都属于这一类。在Command中,令牌代表一个请求;而在Memento中,它代表在一个对象在某个特定时刻的内部状态。在这两种情况下,令牌都可以有一个复杂的内部表示,但客户并不会意识到这一点。但这里还有一些区别:在Command模式中多态很重要,因为执行Command对象是一个多态的操作。相反,Memento接口非常小,以至于备忘录只能作为一个值传递。因此它很可能根本不给它的客户提供任何多态操作。

通信应该被封装还是被分布

Mediator和Observer是相互竞争的模式。它们之间的差别是,Observer通过引入Observer和subject对象来分布通信,而Mediator对象则封装了其他对象间的通信。

在Observer模式中,不存在封装一个约束的单个对象,而必须是由Observer和subject对象相互协作来维护这个约束。通信模式由观察者和目标连接的方式决定:一个目标通常有多个观察者,并且有时一个目标的观察者也是另一个观察者的目标。Mediator模式的目的是集中而不是分布。它将维护一个约束的职责直接放在一个中介者中。

我们发现生成可复用的Observer和subject比生成可复用的Mediator容易一些。Observer模式有利于Observer和subject间的分割和松耦合,同时这将产生粒度更细,从而更易于复用的类。另一方面,相对于Observer,Mediator中的通信流更容易理解。观察者和目标通常在它们被创建后很快即被连接起来,并且很难看出此后它们在程序中是如何连接的。如果你了解Observer模式,你将知道观察者和目标间连接的方式是很重要的,并且你也知道寻找哪些连接。然而,Observer模式引人的间接性仍然会使得一个系统难以理解。

Smalltalk中的Observer可以用消息进行参数化以访问subject的状态,因此与在c++中的Observer相比,它们具有更大的可复用性。这使得Smalltalk中Observer比Mediator更具吸引力。因此一个Smalltalk程序员通常会使用Observer而一个C++程序员则会使用Mediator。

对发送者和接收者解耦

当合作的对象直接互相引用时,它们变得互相依赖,这可能会对一个系统的分层和重用性产生负面影响。命令、观察者、中介者,和职责链等模式都涉及如何对发送者和接收者解耦,但它们又各有不同的权衡考虑。

命令模式使用一个Command对象来定义一个发送者和一个接收者之间的绑定关系,从而支持解耦,如下图所示。
设计模式——设计模式选择——行为型

Command对象提供了一个提交请求的简单接口(即Execute操作)。将发送者和接收者之间的连接定义在一个单独的对象使得该发送者可以与不同的接收者一起工作。这就将发送者与接收者解耦,使发送者更易于复用。此外,可以复用Command对象,用不同的发送者参数化一个接收者。虽然Command模式描述了避免使用生成子类的实现技术,名义上每一个发送者一接收者连接都需要一个子类。

观察者模式通过定义一个接口来通知目标中发生的改变,从而将发送者(目标)与接收者(观察者)解耦。Observer定义了一个比Command更松的发送者一接收者绑定,因为一个目标可能有多个观察者,并且其数目可以在运行时变化,如下图所示。
设计模式——设计模式选择——行为型
观察者模式中的Subject和Observer接口是为了处理Subject的变化而设计的,因此当对象间有数据依赖时,最好用观察者模式来对它们进行解耦。

中介者模式让对象通过一个Mator对象间接的互相引用,从而对它们解耦,如下图所示。
设计模式——设计模式选择——行为型

一个Mediator对象为各Colleague对象间的请求提供路由并集中它们的通信。因此各Colleague对象仅能通过Mediator接口相互交谈。因为这个接口是固定的,为增加灵活性Mediator可能不得不实现它自己的分发策略。可以用一定方式对请求编码并打包参数,使得CoIleague对象可以请求的操作数目不限。

中介者模式可以减少一个系统中的子类生成,因为它将通信行为集中到一个类中而不是将其分布在各个子类中。然而,特别的分发策略通常会降低类型安全性。

最后,职责链模式通过沿一个潜在接收者链传递请求而将发送者与接收者解耦,如下图所示。
设计模式——设计模式选择——行为型

因为发送者和接收者之间的接口是固定的,职责链可能也需要一个定制的分发策略。因此它与Mediato一样存在类型安全的问题。如果职责链已经是系统结构的一部分,同时在链上的多个对象中总有一个可以处理请求,那么职责链将是一个很好的将发送者和接收者解耦的方法。此外,因为链可以被简单的改变和扩展,从而该模式提供了更大的灵活性。

总结

除了少数例外情况,各个行为设计模式之间是相互补充和相互加强的关系。例如,一个职责链中的类可能包括至少一个Template Method的应用。该模板方法可使用原语操作确定该对象是否应处理该请求并选择应转发的对象。职责链可以使用Command模式将请求表示为对象。Interpreter可以使用state模式定义语法分析上下文。迭代器可以遍历一个聚合,而访问者可以对它的每一个元素进行一个操作。

行为模式也与能其他模式很好地协同工作。例如,一个使用Composite模式的系统可以使用一个访问者对该复合的各成分进行一些操作。它可以使用职责链使得各成分可以通过它们的父类访问某些全局属性。它也可以使用Decorater对该复合的某些部分的这些属性进行改写。它可以使用Observer模式将一个对象结构与另一个对象结构联系起来,可以使用State模式使得一个构件在状态改变时可以改变自身的行为。复合本身可以使用Builder中的方法创建,并且它可以被系统中的其他部分当作一个Prototype。

设计良好的面向对象式系统通常有多个模式镶嵌在其中,但其设计者却未必使用这些术语进行思考。然而,在模式级别而不是在类或对象级别上的进行系统组装可以使我们更方便地获取同等的协同性。