Behavioral Patterns(行为模式)涉及对象之间任务的分配以及完成这些任务的算法,它不仅描述了对象的模式或者类的设计模式,还描述了对象或类之间交互的模式,使用这种设计模式可以使得程序员从繁杂的控制流当中解脱出来,只专注于对象之间的交互。
Behavioral Patterns可以分为两类设计模式:Behavioral Class Patterns(行为类模式)和Behavioral Object Patterns(行为对象模式)。
Behavioral Class Patterns通过类继承的方式将任务在类之间分配,这类模式包括:Template Method Pattern(模版方法模式)和Interpreter Pattern(解释器模式)。
Behavioral Object Patterns通过对象之间的组合而不是类的继承实现对象之间的任务分配,描述了一组对等对象通过协作完成一个任务,这个任务是其中任何一个对象都无法单独完成的。既然对象之间需要协调,那么一个对象可能需要了解同一个group其他对象,那么这类设计模式的一个重要的问题就是如何让对等对象相互了解。这类设计模式主要包括:Mediator Pattern(中介模式),Chain of Responsibility Pattern(责任链模式),Observer Pattern(观察者模式),Strategy Pattern(策略模式),Command Pattern(命令模式),State Pattern(状态模式),Visitor Pattern(访问者模式),Iterator Pattern(迭代器模式)和Memento Pattern(备忘录模式)。
Chain of Responsibility Pattern:
Chain of Responsibility Pattern(责任链模式)引入多个对象来处理请求,任何一个对象都有可能处理一个请求,但是请求并不知道具体被哪个对象处理,从而将请求的发送者和接受者去耦合。Chain of Responsibility Pattern将这些接收对象连接成链,并将请求沿着这条链传下去,知道遇到一个对象处理它。
通常会有一个基类Handler,在这个类里面会有一个虚方法Handle,还有一个succesor。Handle默认实现为将请求传递给succesor处理,它也可以被它的子类重新实现。每个子类在接受到请求之后,首先判断自己是否能够处理这个请求,如果可以,就直接处理,否则调用基类Handler的Handle方法,将请求传递给succesor处理。
这个设计模式的大致结构如下:
Command Pattern:
Command Pattern(命令模式)将request(请求)封装成对象,不同的对象向不同的request接收者出发不同的request,从而得到不同的响应。
我们所用的有图形界面的软件,一般都会有Button(按钮),当我们点击按钮的时候软件通常会做出响应。这个过程可以看作:我们点击Button,Button感应到(并且可能发送请求到接收者),然后做出响应。实现这一功能,简单地,直接将响应过程的代码写到Button里面,每次Button被按下,就会调用Button自带的响应程序去响应。但是这个过程有个明显的缺点就是每个Button都要实现其响应函数,而且如果两个Button所实现的功能差不多甚至完全一样,也要为这两个Button分别实现响应程序,严重影响了代码的重用性,而且实现和维护起来极其繁杂,而且将request的发送者和接收者捆绑起来了,耦合度很高。
后来就出现了callback function(回调函数),这是一种面向过程的机制,很好地解决了代码重用问题,并且将request的发送端和接收端去耦合了。而使用Command Pattern实现这一功能则是面向对象的机制,相比于callback function,Command可以看作成一个整体的对象,对象不仅包含响应方法,还包括一些属性(比如状态等),有了这些属性,Command不仅能向callback function一样将request的发送和接收响应过程去耦合,还能记录所发送和响应命令的记录,甚至还能支持取消操作。由于Command将命令看成一个整体,那么还可以将这些Command对象组成一个复杂的对象从而实现复杂的操作。
在Command Pattern中,首先有一个Command基类,包含一个抽象接口Execute,在这个基类的基础上可以派生出很多的具体子类ConcreteComand,每个子类可以重新实现Execute方法,并且每个子类还可以定义自己的Receiver(命令的接收者),在需要的情况下还可以定义状态属性。每定义一个Button,就将一个特定的ConcreteCommand实例Attach到这个Button,点击Button,就调用ConcreteCommand实例中的Execute方法执行request的发送。
Command Pattern的大致结构如下:
Interpreter Pattern:
Interpreter Pattern(解释器模式)主要用于完成解析文法类似的工作。
给定一个语法以及该语法中的语句,计算该语法中语句的具体值。有多种方法可以解决这个问题,比如将文法直接转换成状态机,而且效率比Interpreter Pattern高。而且,在Interpreter Pattern中,对于每个语法规则至少要实现一个对应的类,一旦文法规则复杂起来,使用Interpreter Pattern将导致这些类很多以致于不可控。但是,Interpreter Pattern的方法实现起来相对简单。
在Interpreter Pattern中,主要分为两部分:Context和Expression。Expression表示表达式,也可以理解为遵循某一文法的语句。而Context是指当前的上下文,比如某个变量的具体值是多少,通过这些值可以计算出整个表达式或句子的值。
在Expression中,首先有一个表达式基类AbstractExpression,这个基类里面有一个公共的抽象的接口Interpret(context)----根据当前的context解析表达式的值。AbstractExpression派生出两个子类TerminalExpression(终结表达式,可以作为句子的尾部)和NonterminalExpression(非终结表达式,不可作为句子的尾部)。NonterminalExpression还可以派生出具体的表达式子类。这样就形成了一棵文法树,使用文法树,不仅可以解析具体表达式的值,还可以根据需要添加具体的操作。
Interpreter Pattern的大致结构如下:
Iterator Pattern:
Iterator Pattern(迭代器模式)为数据集合对象提供了一种访问方式,通过这种方式,可以使得数据集合对象的一些接口或者属性值不暴露出来。
在Java里面,List就可以看成一个Iterator(迭代器),它是一个基类(在Java里面是一个接口),这个类里面提供了访问具体数据集合对象的抽象接口,比如get、add等,它有两个常见的子类(或者说有两个常见的类对List这个接口进行了实现)ArrayList和LinkedList,这两个子类分别存储着不同结构的数据集合对象,一个是数组,另一个是链表。但是他们可以通过对List类提供的抽象接口重写来实现他们具体类所需要的功能,然后在访问不同结构的数据集合对象时,就可以通过相同形式的迭代器接口访问。
在Iterator Pattern中,主要有两部分组成:Aggregate和Iterator。第一部分Aggregate是数据集合类,提供了一些数据集合通用操作接口,在Aggregate的基础上,还可以派生出各种各样的具体子类,代表不同的数据集合结构,具体子类可以对Aggregate提供的抽象接口重新实现。第二部分Iterator是用于访问Aggregate的迭代器,提供了一些常用的访问数据接口,比如First(),Next()等等,同样地Iterator可以派生出各种具体的迭代器子类,每个迭代器子类都可以根据需要对Iterator提供的抽象方法进行重新实现。
Iterator Pattern的大致结构如下:
Mediator Pattern:
Mediator Pattern(适配器模式)定义了一个对象,这个对象将一系列对象的交互方式封装起来,从而降低了交互对象之间的耦合度。
面向对象程序设计通常鼓励将一个大的任务分成很多个小的任务,每个小的任务通过和其他小任务交互完成各自的功能。这种方式主要有三个方面的缺点:1、这样产生许多小的任务对象之间的连接(或者引用),最坏情况下,甚至任一两个小任务对象之间都会有一对连接(相互包含各自对象的引用),如果小任务个数为n的话,最坏情况下连接数则为n(n-1)。2、任意一个小任务对象改变时,可能影响其他小任务对象的行为,牵一发而动全身,这时被改变的小对象需要通知所有引用它的小对象,而且需要每个小任务对象都实现和其他小任务对象的交互方式,这会使小任务类的设计变得非常复杂。3、而且,如果按照这种方式实现的话,对于每个大的任务,重新实现小对象之间的交互。具体地,当一个小对象改变时,在不同的大任务中,受到影响的对象也会不同,那么需要重新设计小任务对象。如果通过派生子类的方式去实现,最坏可能需要派生出n个子类,这点充分说明了在这种方式中对象之间的耦合度之高。
而在Mediator Pattern中,包含了一个重要的对象----Mediator(适配器)。在将大任务划分成多个小任务对象之后,这些小任务对象只需要和Mediator交互就行了,这就将对象之间的连接数降下来了,最多只需n个连接。在某个小任务对象发生改变时,只需将改变信息发送到Mediator对象,这个对象自然会通过改变的信息调整其他小任务对象,交互方式非常简单。而且,如果想完成另外一个大任务是,只需要重新设计一个Mediator对象就行了,而不是重新设计出n个小任务子类。
Mediator Pattern主要是将一些分布式的工作集中到一个Mediator对象中来完成,它的大致结构如下所示:
Colleague就是上面所说的小任务对象类,Mediator Pattern中的交互方式大致如下所示:
Memento Pattern:
Memento Pattern(备忘录模式)主要可以用来抽取并保存数据对象的内部状态,使得数据对象在需要的时候恢复到以前的某个状态,并且又不需要破坏对象的封装性。
在使用文本或图形编辑器的时候,经常会用到一个undo(撤销)操作,完成这一操作首先需要知道撤销以后的内容是什么,这就需要记录并保存撤销之前的状态。但是通常情况下,对象会将自己的属性封装起来,使得外部访问这些属性很不方便甚至是不能访问;而且把这些属性暴露出来又会破坏对象的封装性。这是一个trade-off,一个是为了维护封装性不暴露属性,这样就无法记录数据对象的状态;一个是为了暴露属性来记录数据对象的状态,但是却暴露了封装性。Memento Pattern就可以完美地解决这个问题。
Memento顾名思义表示备忘录,在数据对象(可以看成Originator)发生改变时并且在需要的情况下,就可以创建一个memento来保存当前对象状态,在数据对象连续变化的过程中,不断地创建一些memento来保存一些关键时间点的数据对象状态,因此也可以将memento看成数据对象的snapshot(快照)。在数据对象需要恢复到之前的某个状态时,调用对应的memento,查看里面的状态属性,就可以恢复到之前的状态。可以根据需要将memento保存成一定的结构,不妨将这个结构统成为Caretaker,简单地可以将Caretaker设计成一个队列。现在还只是说明了用Memento Pattern如何实现数据对象的恢复操作,但是数据对象属性的封装性并没有谈及。在很多面向对象语言里面可以通过设置访问权限来实现,在C++里面,可以将数据Memento对象的属性设置为protected的,并且将Originator设为友元类,那么在恢复状态的时候,Originator就可以访问Memento对象的属性,而其他地方比如Caretaker却只能保存或者传递Memento对象,而不能访问Memento对象的属性,这样就维护了数据对象的封装性。
Memento Pattern的大致结构如下:
Observer Pattern:
Observer Pattern(观察者模式)定义了一个一对多的依赖关系,当一个对象的状态被改变时,所有依赖它的对象就会自动地被通知并且做出相应的改变。
在面向对象里面,通常会鼓励将一个系统划分成很多个小的部分,这样有利于代码的重用、模块之间的独立性等等。但是划分成很多个小模块之后,可能出现这些小模块之间的数据一致性问题。一个模块改变了数据,而其他对象可能也引用了这个数据,如果其他对象没有即时更新这个数据,就会导致同一个数据的值不一致。解决这个问题,简单地,可以每个小模块和其他可能相关的(引用了相同的数据)小模块建立连接,当一个小模块改变一个数据时,就通知其他引用了这个数据的小模块更新数据。这样做虽然能暂时地解决问题,但是有几个缺点:1.整个系统需要建立很多连接,假设n为模块数,极端情况下需要建立n*(n-1)个连接,而且每增加一个模块,就需要查看和哪些模块引用了相同的数据然后与它们建立连接,这样做使得整个系统变得非常复杂,难以维护,以及整个系统的效率也不高。2.每个模块都需要保存与自己相关的其他模块,需要知道自己到底与哪些模块相关,而且需要自己实现和其他模块的交互,这样做不仅需要花费较多的内存,模块的代码实现复杂,代码重用率也不高。3.模块之间的耦合度极其高,使得整个系统的所有模块都糅合在一起,独立性非常差。Observer Pattern就可以很好地解决这个问题:将所有模块看成Observer(观察者),另外再新建一个模块作为数据的存储池(Subject),所有的Observer只需要和Subject建立连接就可以了,并且所有Observer只能从Subject读取数据。每次一个Observer改变某个数据时,Subject就会通知其他对象,其他对象根据需要改变相应的数据值。这样做,使得整个系统的连接数变小,易于维护,模块之间的交互也非常简单方便(只需要和一个对象(Subject对象)发生交互),模块之间的独立性也非常高。使用Observer Pattern不仅可以解决这个问题,还能够完美地克服上述方法的几个缺点。
具体地,举个例子,一张统计表,可以用很多种方式将里面的数据显示出来,比如扇形统计图,柱形统计图,统计表等等。可以将显示方式看成一个系统的不同显示模块,这些显示模块都使用同一个统计表的数据。如果使用简单方法,就需要在不同显示模块之间建立连接,当一个显示模块修改摸个数据时,就需要通知其他显示模块,这样虽然能够解决,但是在前面已经讲过这种方法的缺点了。如果用Observer Pattern解决这个问题的话,只需要建立一个Subject类,保存统计表数据,其他的显示模块都从Subject类里面取数据,某个显示模块改变数据时,只需要通知通知Subject,Subject类再去通知其他的显示模块。
在Observer Pattern中,首先有个Subject类,可以看成数据交互枢纽,通常会提供三个主要的抽象接口:Attach(添加Observer),Detach(删除Observer),Notify(通知Observer更新数据)。在Subject类的基础上,可以根据需要派生出具体的子类ConcreteSubject,每个子类都有自己的数据状态,并且实现数据状态的操作。另外地,还包括Observer类,这个类通常会提供一个抽象接口Update。同样地,Observer类可以派生出其他具体的子类ConcreteObserver,ConcreteObserver通常都会有自己的数据状态,并且会重新实现Update。当某个数据发生改变时,Subject就会通过Notify通知各个Observer调用各自的Update更新数据状态。
Observer Pattern的大致结构如下:
State Pattern:
State Pattern(状态模式)将对象的内部状态封装到一个状态对象,当这个对象的状态对象发生改变时,那么他的一些方法的运行方式也会发生改变。
有的时候一个对象需要根据自身当前的属性状态来确定自己的操作行为,以及状态转移。比如,当一个对象的状态为A时,它应该做OptionA操作,并且将状态切换为B;当状态为B时,它应该做OptionB操作,并且将状态切换为A。整个过程牵涉到两步:1、根据状态做合适的操作行为;2、确定转移状态。实现这一功能通常有两种方法:1.通过条件语句判断具体的状态,做出相应的操作,并切换状态。2.使用状态转移表。
第一种方法,如果状态较多的时候,条件转移部分将会比较难以维护,而且通过条件语句来实现状态的转移,也不直观。而第二种方法,相对第一种方法,把状态转移部分简化了,使得状态转移实现和维护都非常方便,但是它只关注了状态的转移,转移过程中的想要添加一些必要的操作都比较困难,对象想根据当前的状态做一些相应的操作也十分困难,而且状态的转移也不够直观。这两种方法代表了两个极端,一种方法使得在状态转移过程中添加必要的操作比较方便,但是过多的条件语句不好维护,另一种方法使得状态转移的实现和维护非常方便,但是却难以在转移过程中添加额外的操作。而State Pattern却能够将这两个看似不可调和的矛盾巧妙地消除,并且吸收两种方法的优点。在State Pattern中,只需要将状态封装到对象,每次将所需要做的操作交给对应的对象,对象完成后自动转移状态。
具体地,举个例子,在TCP协议中会包含三个主要的状态:Established(创建)、Listening(监听)和Closed(关闭)。当TCP连接处于这三个不同的状态时,所做的操作也是不同的,不同状态的转移也是不同的。要实现这个功能简单地会想到通过条件语句判断来实现,或者通过状态转移表来实现,但是两者的缺点在前面已经讲过。如果用State Pattern来实现,则只需要创建一个TCPConnection对象代表当前连接,TCPEstablished、TCPListen以及TCPClosed分别代表三个状态对象,这三个对象拥有相同的父类(TCPState),TCPConnection有一个TCPState指针,TCPConnection根据TCPState指针指向的具体子类做出具体的操作行为,并且做出具体的状态转移。这样实现起来不仅方便维护,而且状态转移也比较直观。
在State Pattern中,有一个Context类,包含一个抽象接口Request,以及一个状态指针state;另外还有一个State基类,包含一个抽象接口Handle,在这个基类的基础上还可以派生出一些具体的子类ConcreteState,这些子类可以根据需要重新实现Handle接口。Context对象可以根据状态指针指向的具体状态对象,调用状态对象的Handle方法做出具体的操作或者状态转移。
State Pattern的大致结构如下:
Strategy Pattern:
Strategy Pattern(策略模式)定义了一系列的算法,并将其中每个算法封装起来,使得他们之间可以相互交换。使用Strategy Pattern可以让算法独立于它的使用者。
设想一个类,希望通过不同的方式来实现某个操作(OperateA),通常有两种方式:1、在同一个类定义多个不同的操作方式,然后在执行OperatorA的时候可以确定用何种方式执行;2、派生出不同的子类,每个子类根据不同的操作方式重新实现OperateA。两种都能解决这个问题,但是都有各自的缺点:1、第一种方法,当操作方式很多的时候,会使得一个类过大,不易于维护。2、第二种方法,虽然说每个类都不大,但是当操作方式多的时候,类却很多了,也难以维护。3、它们也有一些共同的缺点,使得操作方式(算法)完全依赖于使用算法的类,毫无独立性,而且当其他类也想使用相同的算法时,需要重新定义,这是非常麻烦的事情。
而使用Strategy Pattern就能够很好地解决这个问题:将一类操作方式(算法)抽取出来,独立封装起来,并且可以通过相同的接口去调用,每次想使用某种操作方式时,只需要通过算法公共接口去调用即可,而且算法的具体实现和他的使用者毫无关系,使得算法独立于它的使用者。同时,同一个算法可以被多个不同的使用者(类)引用,通过这种方式,当使用者想扩展一种操作方式(算法)时,只需要在算法类里面添加,而不需要在每个引用这个算法的使用者当中重新实现,非常方便。
在Strategy Pattern中,包含一个基类Strategy,这个基类提供了一个抽象接口AlgorithmIntereface(),在Strategy的基础上可以根据需要定义不同的具体子类ConcreteStrategy。在使用算法的地方,只需要声明一个Strategy指针,通过将这个Strategy指针指向具体的子类,然后通过这个子类调用自身的AlgorithmInterface()完成具体操作。
通常情况下,代码包含非常多的连续条件语句块时,就需要考虑使用Strategy Pattern了。
Strategy Pattern的大致结构如下:
Template Method Pattern:
Template Method Pattern(模版方法模式)在一个操作中定义了一个算法框架,并且将这个框架当中的一些步骤交给子类去完成。使用Template Method Pattern,让子类重新定义算法中的一些具体步骤,而不改变整个算法框架。
有时在写程序的过程中会发现,有些不同操作之间的结构大致相同,但是有些具体步骤却不相同,这种情况下,在写的时候,不同操作之间虽然有较多的代码不一样,但还是会让人有一种在重复造轮子的感觉。Template Method Pattern就提供了一种代码结构组织思想,使得在这种情况下,让代码更具重用性。
Template Method Pattern的关键思想就是将不同操作模块之间的共同点和差异点找出来。将共同点用Template Method组织并固定成一个算法框架;而将差异点提取并且封装起来,并使得不同操作之间的差异点具有相同的接口,根据需要对这些差异点接口进行不同的实现,最后在组织并固定好的算法框架中调用这些接口及可以了。这样做,将算法的框架和框架里面一些具体操作的实现分离开来,使得两者独立,有利于代码的维护和重用。
在Template Method Pattern中,首先定义了一个AbstractClass类,里面包含一些抽象的接口,其中一个接口用于对算法框架的组织、固定和实现----TemplateMethod(),其他的接口主要是针对算法框架一些具体步骤而声明的,比如PrimitiveOperation1,PrimitiveOperation2等等。在AbstractClass类的基础上,还可以派生出很多的具体子类,这些具体子类根据需要可以对一些具体步骤接口(比如PrimitiveOperation1,PrimitiveOperation2)进行重新实现,而通常不需要对TemplateMethod(算法框架)进行重新实现,也就是说TemplateMethod通常是由基类定义并固定的。
Template Method Pattern的大致结构如下:
Visitor Pattern:
Visitor Pattern(访问者模式)代表一种执行在一个对象结构元素上的一种操作。使用Visitor Pattern,可以使得在不改变结构对象元素类的情况下添加一些新的操作。
一个节点类,派生出多个子节点类,某个节点对象想要完成某种操作,直观地,可以直接在该节点类添加一种方法,但是如果是在具体子类里面实现的话,就不能通过父类指针调用,会将具体子类暴露出来,破坏了封装性;如果是在父类里面实现,随着对象的使用Context(情况,上下文)不同,会需要添加新的方法,而这些方法可能与一些具体的子类毫无关系,以致于“污染”了一些子类,并且,每次添加一个新的方法,将会导致整个类结构(父类以及父类的所有子类)重新编译。如果把这种操作提取出来放到一个Visitor类里面定义,当该对象需要根据Context添加某种操作时,只需要定义一个Visitor子类去实现,而不需要重新编译对象类,而且不会出现类“污染”问题,因为具体的方法根本就不在节点类里面,节点类只提供一个接口去接收Node指针,并通过Node指针调用具体的操作,这就是Visitor Pattern的主要思想。
但是,Visitor类通常是根据节点类结构去设计的,如果节点类结构发生改变的话,Visitor也将需要很多的修改,当节点类变化频繁时,则Visitor Pattern就不适用了。所以使用Visitor Pattern要考虑的两个关键问题是:节点类结构是否经常改变?如果经常改变,就应该将操作方法定义在节点类。节点类是否经常需要根据不同的Context而为某些或者全部节点添加新的方法?如果是的,则Visitor Pattern是一个比较好的选择。
在Visitor Pattern中,包含两个层级结构:Visitor类层级结构,Element(元素)类层级结构。在Visitor层级结构中,首先定义一个Visitor基类,在这个基类中根据不同元素提供不同的抽象访问(或者操作)接口VisitConcreteElementA、VisitConcreteElementB。在Visitor基类的基础上,还可以派生出不同的子类,每个子类可以根据需要对不同元素的抽象访问接口进行具体的实现。在Element层级结构中,首先定义一个Element基类,这个基类里面定义了一个Accept(visitor)接口,根据需要Element可以派生出不同的子类代表不同的元素,每个具体元素子类都可以重新实现Accept,当某个元素Element调用Accept的时候,就可以使用传递进来的visitor参数对象调用相应的操作。
Visitor Pattern的大致结构如下:
参考文献:
[1] Erich Gamma, Richard Helm,Ralph Johnson,John Vlissides. Design Patterns: Elements of Reusable Object Oriented Software.Pearson Education.