# 13.3 行为模式——君子之交淡如水
结构模式关注的是对象静态的结构,行为模式(behavioral pattern)关注的是对象的动态行为。
对象行为的一种重要体现是对象之间的通信,这恰恰是OOP的一大弱点。
准确地说是命令式OOP的弱点,即一个对象必须在获得另一个对象的标识(identity)后方能向其发送消息。
责任链模式(chain of responsibility pattern)
P427 图13-14
在责任链模式中消息接收者可能并不直接处理请求,而是转给后继者。
换言之,消息发送者虽然知道哪一类对象是接收者,但却不知道哪一个或哪一些对象实际参与了请求的处理。
场景
责任链模式广泛应用于事件模型(event model)中。
各种机构采取的层层审批制度,就是一种责任链。
Java Servlet中的filter机制,可以把servlet过滤器写在配置文件中,不需要硬编码。
异常处理(exception handling)机制,可chenweil语言级别的责任链。
责任链可以在运行期改变,它实际上实现了动态层级。
异常处理
许多人经常在何时抛出异常,该抛出何种异常,处理完异常之后该不该继续上抛等问题上狐疑不决。
其一是没有责任链意识——有些事情是需要分模块分时间分批次来完成的;
其二是没有抽象层次的意识——不同抽象层次的对象处理不同层次的异常,必要时通过包装提升异常的抽象层次交由上级处理。
假如一个方法捕捉到一个异常,通常有3种情形:
一,不在改方法范围的职责以内。该方法应直接讲异常连同必要的信息以更抽象的形式包装起来,然后抛给更高层。
二,部分由该方法负责。该方法对异常进行一些处理,然后重复情形一的行为。
三,完全由该方法负责。此时方法内进行相应的处理。不再为此抛出异常。
如此一个异常从底层开始冒泡,一步步走向高层,一步步地被消化处理。
异常的抽象层次相应的也进化的越来越高级,越来越离开底层,直至最终用户。
与装饰者模式
装饰者模式关心的是职责的结合,且更侧重结构。
责任链模式关心的是职责的分配,且更侧重行为;
它们可以自然的融合在一起,比如职责链的每一环正好是在为某对象进行装饰和包装。
命令模式(command pattern)
P429 图13-15 命令模式
也能实现发送者与接收者之间的解耦。
基本思想是
把请求或命令封装为一个对象,即命令对象,交由请求的发送者或命令的调用者控制。
一方面,命令对象包涵了执行命令的全部信息——请求的接收者、方法和方法参数,可谓万事俱备,只等一声令下。
另一方面,命令对象是以统一接口的形式出现的,下令者不必知道命令的具体类型,更不必知道实际的执行者以及执行方式,唯一要把握的是下令的时机。
从形式上看一个命令与C++中的函子(functor)或函数对象很相似,只是前者的执行方法是execute,后者是函数调用算符。
不过函子通常只是函数的包装,不一定有receiver对象。
命令模式在Invoker与Receiver之间插入了一个间接的对象——命令,改对象用抽象接口把命令封装起来,可以保护各种变化,包括命令的执行者、执行方式、执行时间等。实现命令下达者与执行者在时间与空间上的解耦。
实际上,这就是异步回调,或者说命令实质上是OO化的回调。
回调的本质是把代码当数据使用,作为回调函数的对象化包装,命令对象也是如此。
正由于命令的数据化封装,使得命令成为头等对象(first-class object),可以保持、排队、记录日志、异步执行、异地执行、动态选择或修改,还可以执行还原(undo)操作,甚至可以把若干命令组合成宏命令。
它在可视化编程中应用得尤其广泛,在需要定时执行一些列行动和事务的应用中也是大有作为。
观察者模式
相比命令模式,可以把消息同时发送给多个接收者。(P430 图13-16)
在观察者模式中,Subject对象虽然维护了一组Observer对象,
但由于后者是以抽象的接口形式登记的,因此消息发送者——Subject对象与消息接收者——Oberver之间不存在紧耦合。 它是好莱坞原则或控制反转原则的应用,尤其适合低层模块对高层模块的反向控制。 在事件驱动模型中,Subject代表低层的事件源,Observer代表着高层的事件处理器。 在MVC模型中,Subject代表低层模型,Observer代表高层的视图。 中介者模式 (P431 图13-17) 利用纯虚原则、间接原则和保变原则,构造一个信息交换中心,为众多的的对象提供了交流平台。 与观察者模式的不同之处在于,中介者模式在通信与同步的职责分配上, 是集中式的——Mediator对象管理所有的通信和同步协议,观察者模式是分布式的。 它们也经常结合使用,Mediator对象可以利用观察者模式与Colleague对象进行交流,当Subject对象与Observer对象之间的同步逻辑比较复杂时也可能会引入Mediator对象。 举个例子:通行的两个人在公园走散了,在没有通信设备的情况下,他们如何联系? 其中一个人到公园的广播台,通过广播找到另一个人。 这两个人是Colleague对象,公园广播站是Mediator对象。 同时,广播台有事Subject对象,公园的每个人——包括两位主角都是Observer对象。 外观模式 外观模式与中介者模式,都是利用一个中介对象, 把多对多的交流模式转化为多对一和一对多的交流方式。 它们的区别也很明显: 请求的发送者和接收者之间的关系不同:在外观模式中是单向的主客关系,在中介者模式中是双向的同事关系; 中间对象的角色也有分别:在外观模式中是客户访问子系统的门户,在中介者模式中是彼此交流的平台。 另外,外观模式是重简化通信接口,故属接口模式;中介者模式着重简化通信方式,故为行为模式。 状态模式(state pattern) 行为模式不都是以通信方式为主题的,如状态模式(state pattern)关心的是一个对象的自身行为如果随着自身状态的改变而改变。(P433 图13-18) 利用间接的State对象来封装状态的变化,它把不同状态对应的不同行为包装到不同的模块之中。 根据实际的需求,有的为客户提供直接修改状态的接口,有的只允许其State类型的成员发出状态迁移的请求,有的在运行期间能自动切换状态。 如果状态类是无状态的(stateless),通常会采用单例模式。 备忘录模式(memento pattern 如果说状态模式关注的是对象状态的演进,那么备忘录模式(memento 前面提到命令模式可以用来支持还原(undo)操作,但要实现还原,就需要相关对对象状态的历史记录,备忘录模式可以助一臂之力。(P433 图13-19) Originator【n. 发起人;起源;起因】 对象时备忘的主体和制造者,它的状态为Memento对象所封装, 后者由 Caretaker【n. 看管者;看门人;守护者】对象负责保护和管理。 由于Originator 类是有回滚和快照的接口,不仅能支持单步还原,还支持批量还原。 备忘录模式可以保持对象的状态,序列化和克隆不也能保持对象的状态码? 即使不考虑序列化的性能问题,版本问题,以及克隆的拷贝深浅问题,它们有一点无法与备忘录模式相提并论: 它们只能保存对象的状态,却不能以此来恢复原对象的状态。原因很简单,对象的状态恢复都是在不改变对象标识的前提下而言的,而经过反序列化和克隆的对象产生了新的标识。 另外,备忘录模式也不必局限于用作对象状态恢复的工具。它也能把Originator类的部分状态的版本信息在不破坏封装的前提下移交客户管理,从而减轻后者的负担。 许多设计模式都是为了克服编程语言的一些缺陷或局限而设计的。 有些是为了克服new运算符和构造器的局限,如工厂模式; 有些是为了克服内存分配的局限,如享元模式; 有些是为了克服函数不是头等公民的局限,如命令模式; 有些是为了克服无法动态继承的局限,如状态模式; 有些是为了克服过程式语言、静态类型语言和静态语言语法限制而造成的对象通信上的局限,如责任链模式、观察者模式、中介模式等。 访问者模式 (P440 图13-20 访问者模式) 访问者模式是为了克服OOP语言的单分派(single dispatch)的局限而设计的。 分派是为了一个调用点(Call Site)确定相应调用方法的过程。 如果函数名或方法名是唯一的,该过程十分简单。如果不唯一,不同的语言可能有不同的算法,通常根据参数的类型和个数来决定的。 若分派能再编译器决定,则是静态分派,如重载多态(overload Polymorphism)和参数多态(parametric Polymorphism)。 分派若在运行期决定,则是动态分派,如子类型多态(subtyping Polymorphism)。 C++、Java和C#等语言采用的是同样的动态分派机制: 在运行期根据一个方法的接收者——即方法所属的对象——的实际类型来决定到底该调用哪种方法。 由于这类分派仅仅取决于一个类型,即方法的接收对象的类型,故称为单分派。 从编译器的角度看,在进行方法分派时是同时考察参数类型和实例类型的。 区别在于,对应前者的考察在编译后即结束了,而对于后者的考察还要延续到运行期。 这种迟绑定虽然可能有一定的性能损耗,却带来了极其重要的多态进制。 如果分派取决于两个参数的类型,则称双分派,它是多分派的一种特别情况。 目前只有CLOS、Clojure、Dylan、Groovy等少数语言支持。 (P439 文件结构双分派实现) 在语句file.accept(visitor); 中,通过Java 本身的单分派找到实际的accept 方法,实现file的多态; 接着在相应的accept方法中的语句visitor.visit(this);中, 再次利用单分派找到了实际的visit方法,实现visitor的多态。 两个连贯的单分派组合成了一个双分派。 File及其子类型代表复杂数据结构的聚合体(aggregate),访问者模式运行客户通过FileVisitor对象对其每一个元素进行访问。 这符合保变原则,聚合体内内部接口发生变化不会影响到外界的访问; 也符合单一原则和关注点分离原则,聚合体主要负责数据结构,而把相关的算法或功能外包给访问者。 文件打印者、统计者、管理者等,添加这些类不会影响原来的文件类,又扩展了它们的功能,非常符合开闭原则。 需要注意的是,被访问者包含的元素的数据表示可以自由变化,但元素的层级结构最好是稳定的。 具体到文件类,Directory对象在维护其子文件无论用集合还是列表或数组,都不会产生影响。 但如果要增加元素类型,改变元素的包含关系,势必会波及访问者。 比如File类增加了一个SymbolicLink的子类,相应的FileVisitor接口及其所有的实现类均应增加visit(SymbolicLink file)的方法。 File的例子还包含一个透明型的复合模式。Directory类是Composite类,而PlainFile类是Leaf类。 这是结构模式和行为模式的完美结合。复合结构通过accept方法向所有访问者开放了接口,也把功能扩充的职责推给了访问者,从此大可固步自封、不思进取了。 迭代器模式 访问一个聚合体或复合结构,除了访问者模式外,还有迭代器模式,或称为游标模式。 用户可以对聚合体或容器以某种次序遍历容器中的元素,至于容器的内部结构、遍历的起止、推进等细节,均毫不知情,也不需要关心。在用户眼中,所谓的容器无非是一串元素。 与代理模式的代理一样,迭代器也是一种抽象的指针。不同的是前者指向一个标量,后者指向一个向量。 遵循间接原则,迭代器作为容器和算法之间的一个中介,既是二者的粘合剂,也是初始算法摆脱对数据结构的依赖,从而更具普适性和重用性。 泛型 一旦算法被抽象出来,泛型范式便可以大展神威;反过来泛型的语法也让迭代器的使用更加方便和安全。 泛型方式与迭代器可谓相得益彰,充分体现在C++的STL、Java的Collection Framework和C#的Collection Classes中。 与访问者模式 在访问者模式中,聚合体自身提供的遍历方式通常是有局限性的,有时干脆把责任推到访问者身上,或者借用迭代器。 在迭代器模式中,聚合体可能提供多个迭代器,并且迭代器的种类繁多,有不同方向、不同规律的遍历方式,还能对访问权限做限制,如只读、只写、部分屏蔽等。 另外,访问者模式中一般至多允许访问者对聚合体中的元素进行修复,而不允许进行增删、替换,而迭代器模式则可能超越此限制。 不过访问者模式有一个迭代器无法比拟的优势,那就是: 访问者在处理元素时,元素的类型非常的具体,从而可充分利用元素的功能; 通过迭代器得到的元素类型则取决于容器里酥油元素中最抽象的那个类型,因而往往不够具体。 策略模式 模板方法模式 解释器模式(interpret pattern) 有时也称 小语言模式(little Language pattern) 该模式的主体是一颗抽象语法树,是一个符合结构。因此,每一个解释器模式都包含复合模式。 反之,每一个复合模式也可看作一种广义的解释器模式,因为它们都在符合形式的类型层级上一致开放了客户感兴趣的接口。 当然二者的不同也是显而易见的:解释器模式针对的是专门性的语言表示,而不像复合模式针对的是一般性的结构表示。 由于解释器模式是把类型层级和语法结构相对应,一般通过类型层级的扩充即继承来实现语法的扩充,因而属于类范围的模式。 相反,复合模式属于对象范围的模式。此外,解释器模式强调动态的编译,属于行为模式;而复合模式强调静态的组成,属于结构模式。 总结 语言有大小之分,大语言通常通用而复杂,小语言专用而简单。 为提高效率,大语言通常需要利用专门的工具,而小语言,乃至小小语言,完全可以自己动手。 设计模式总结