23种设计模式分析(7):行为型模式

2016-09-19 10:23:18来源:oschina作者:abcijkxyz人点击

1.1.21 Strategy策略模式

  Strategy(策略)模式又称Policy模式。GOF《设计模式》一书对Strategy模式是这样描述的:
定义一系列的算法,把他们一个个封装起来,并且使它们可相互替换。Strategy模式使算法可独立于使用它的客户而变化。
  这里的算法并非狭义的数据结构或算法理论中所讨论的KMP、shell sort等算法,而是指应用程序设计中不同的处理逻辑,前面所说的狭义的算法只是其中的一部分。Strategy模式使得算法与算法的使用者相分离,减少了二者间的耦合度,使得算法可独立于使用它的客户而变化;同时,由于设计粒度的减小,程序的复用性也得到了进一步提高,分离出来的算法可以更好地适应复用的需要。
  Strategy模式主要用来将算法实现从类中分离出来,并封装在一个单独的类中。更简单的说,对象与其行为(behaviour)这本来紧密联系的两部分被解耦,分别放在了两个不同的类中。这使得对同一个行为,可以方便的在任何时候切换不同的实现算法。而通过对策略的封装,为其提供统一的接口,也可以很容易的引入新的策略。

  Strategy模式结构图如下:



   图21-1 Strategy模式类图


  策略模式涉及到三个角色:   1、抽象策略(Strategy)角色:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,Context使用这个接口调用不同的算法,一般使用接口或抽象类实现。   2、环境(Context)角色:需要使用ConcreteStrategy提供的算法。内部维护一个Strategy的实例。负责动态设置运行时Strategy具体的实现算法。负责跟Strategy之间的交互和数据传递。   3、具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。实现了Strategy定义的接口,提供具体的算法实现。   Strategy模式以下列几条原则为基础:   1)每个对象都是一个具有职责的个体。   2)这些职责不同的具体实现是通过多态的使用来完成的。   3)概念上相同的算法具有多个不同的实现,需要进行管理。   下面通过一个实例来说明它的具体使用,这个例子是关于数据库连接的。代码如下:

interface DatabaseStrategy { //Strategy:抽象策略
public void process();
}
class MysqlDBStrategy implements DatabaseStrategy { //具体策略
@Override
public void process() {
System.out.println("处理Mysql数据库连接");
}
}
class OracleDBStrategy implements DatabaseStrategy {
@Override
public void process() {
System.out.println("处理Oracle数据库连接");
}
}
class DataBaseManager { //Context角色
public void process(DatabaseStrategy dbStrategy) {
dbStrategy.process();
}
}
public class StrategyClient {
public static void main(String[] args) {
MysqlDBStrategy mysql = new MysqlDBStrategy();
DataBaseManager manager = new DataBaseManager();
manager.process(mysql);
OracleDBStrategy oracle = new OracleDBStrategy();
manager.process(oracle);
}
}  在我们的实际编程中经常会遇到系统要连接的数据库可能不只一种,如果采用传统的方法,即修改连接Url的方法,这种方法确实可行,但是有一个问题要经常修改源代码,不利于以后的维护,那么有没有一种更好的方法呢?答案是有,使用Strategy模式,首先定义一个连接数据库通用的接口(在上面的例子中是DatabaseStrategy),然后再定义实现该接口的具体类(MysqlDBStrategy、OracleDBStrategy),在这些具体类,实现具体的逻辑。最后再定义一个管理数据库连接的类(DataBaseManager),它的内部有一个方法可以接受具体类实例的参数。我们可以看到这个参数是DatabaseStrategy类型的,也就是说它可以接受任何一个实现了DatabaseStrategy接口的类的具体实例(这里运用了对象替换机制,多态的一种),从而完成数据库连接的处理。如果我们还需要处理另外一种数据库如sqlserver,我们只需要建立一个SqlserverDBStrategy类实现DatabaseStrategy接口,把该类的实例传给DatabaseManager的process方法即可。
  小结:Strategy模式是一种定义一系列算法的方法。概念上看,这些算法完成的都是相同的工作,只是实现不同。
  AWT的LayoutManager,是Strategy模式的一个例子。对于GUI而言,每个组件(Component)在容器中(Container)的排放是需要遵循一定的算法的。通常的方法是使用绝对坐标,就像VB,Delphi之类的工具所作的那样,记录每个组件在容器中的位置。这当然会带来一些问题,比如在窗体缩放的时候,就需要手工编码改变组件的大小和位置,以使得原来的比例得以保存。而在AWT中,引入了布局管理器(LayoutManager)的概念,使得布局的方法大大丰富,编码过程也变得简单。
  一个容器,比如Applet,Panel等,仅仅记录其包含的组件,而布局管理器中封装了对容器中组件进行布局的算法,具体地说,就是指明容器中组件的位置和尺寸的大小。通过布局管理器,你只需要确定想放置的组件间的相对位置即可,这一方面简化编码,另一方面也有助于实现软件的平台无关性。
  每一个容器均有一个布局管理器,当容器需要布置它的组件时,它调用布局管理器的方法布置容器内的组件。LayoutManager2继承于LayoutManager,提供更为细致的布局功能,它可以让布局管理器为组件加上约束条件已确定组件如何被布置。例如,为了确定组件被摆放在边框内的位置,BorderLayout在它的组件上加上方向指示。
特别的,通过实现LayoutManager或者LayoutManager2接口,可以很容易实现自定义的布局策略。


图21-2 AWT中的容器和布局管理器的关系


  如果有几个很相似的类,其区别仅仅是在个别行为上的动作不同,这时候就可以考虑使用Strategy模式。这样,通过策略组合,将原来的多个类精简为一个带有多个策略的类。这很符合OO设计的原则:找到变化的部分,并将其封装起来!Strategy模式同样的为子类继承提供了一个好的替代方案,当使用继承机制的时候,行为的改变是静态的,你只能够改变一次。而策略是动态的,可以在任何时候,切换任何次数。更为重要的是,策略对象可以在不同的环境中被不同的对象所共享。以布局管理器为例,虽然每一个容器只有一个布局管理器,但是一个布局管理器可以为多个容器工作。   从结构上看,Strategy模式与State模式有几分相似,但二者所讨论的Context(情景)具有显著的差异。   State模式在于将其状态信息分离出来保存到一个独立的对象中,以便状态信息的获取或状态的转换;Strategy模式在于将可能的算法分离出来,根据需要进行适当的选择。此外,二者的区别还在于,Strategy模式中各个Strategy(算法、策略)往往用于解决相同的问题,即只是解决同一问题的不同“策略”、“途径”,而且,一次只能有一个Strategy为上次应用提供服务。而State模式中的各个State本身往往具有一定的差异,但他们之间存在明显的相互转换的关系,而且这种转换往往会在程序运行过程中经常性地发生,同时存在一个以上State也是可能的。   区别参考:二者的应用场合不同。状态模式用于处理对象有不同状态(状态机)的场合,策略模式用于随不同外部环境采取不同行为的场合。在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转化;而策略模式里,采取何种策略由外部条件决定。所以,有人说“状态模式是完全封装且自修改的策略模式”。至于Bridge,在结构上与前两者都不一样了。要说相似之处,就是三者都有具有对外接口统一的类,展现出多态性而已。   当存在以下情况时可考虑使用Strategy模式:   1、许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。   2、需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法,当这些变体实现为一个算法的类层次时,可以使用策略模式。   3、算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。   4、一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。   具体的应用实例还可以列举一些,如:   1、以不同的格式保存文件;   2、以不同的方式对文件进行压缩或其他处理;   3、以不同的方式绘制/处理相同的图形数据。



1.1.22 Template Method模板方法模式

  《设计模式》一书对Template Method模式是这样描述的:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。这使得子类可以不改变算法的结构而重新定义算法的某些特定步骤。   这里所说的Template跟Generic Programming(范型编程)中讨论的C++的template不是一回事(虽然有一定的相似性),C++的template是一种逻辑复用的方式,它可以不依赖于OO的Inheritance(继承)机制独立存在,因为GP跟OO所讨论的是完全不同的两个方面,虽然二者经常被融合在一起使用。Template Method模式与template不同,它是建立在继承机制 + 虚函数基础上的,它的核心在于在基类中定义好逻辑处理的框架(或称完成一项任务所需依次执行的步骤,或一段通用的处理逻辑),将具体的处理细节交给子类具体实现,从而达到“使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤”的目的。   Template Method模式的结构如下图所示:



图22-1 Template Method模式类图


  其中的参与者比较简单:   1、AbstractClass(抽象类):定义一到多个抽象方法(也可以不是抽象方法,但至少应该是virtual方法。视你的应用需要,如果你的AbstractClass负责实现一个通用版本的算法,各子类对该方法进行进一步细化,则只需定义成virtual方法即可),具体的子类将重定义它们以实现一个算法;而且还实现一个模板方法,来定义一个算法的骨架。该模板方法不仅调用前面的抽象方法,也可以调用其他的操作。各抽象方法往往被定义成protected(保护)成员,以保证它们只被模板方法调用,而TemplateMethod往往被定义成public非虚成员函数。   2、ConcreteClass(具体类):实现父类中的抽象方法以完成算法中与特定子类相关的步骤。   这里的关键是基类中的TemplateMethod方法,因为正是它定义了对各子类对象适用的通用的处理逻辑。   下面给出一个例子:

abstract class QueryTemplate {//抽象类:算法的模板
//Template Method:模板方法,一般为public的,用来执行算法的各步
public void doQuery() {
formatConnect();
formatSelect();
}
//算法的骨架,一般为protected的,由子类来实现它们
//可见,子类并不能改变算法的框架结构,但可以改变算法的实现步骤
protected abstract void formatConnect();
protected abstract void formatSelect();
}
class OracleQT extends QueryTemplate { //实现算法的具体子类
@Override
public void formatConnect() {
System.out.println("格式化Qracle数据库连接");
}
@Override
public void formatSelect() {
System.out.println("格式化Oracle数据库查询");
}
}
class MysqlQT extends QueryTemplate {
@Override
public void formatConnect() {
System.out.println("格式化Mysql数据库连接");
}
@Override
public void formatSelect() {
System.out.println("格式化Mysql数据库查询");
}
}
public class TemplateTestClient {
public static void main(String[] args) {
QueryTemplate oracleQT = new OracleQT();
oracleQT.doQuery(); //调用抽象模板的模板方法,以执行算法
QueryTemplate mysqlQT = new MysqlQT();
mysqlQT.doQuery();
}
}

  在这个例子中,我们定义了一个骨架QueryTemplate,在它的内部定义了一个Template Method和一些步骤(抽象方法),使用Template Method来调用这些步骤。步骤是在子类中实现的。   理解:定义一个抽象类(接口),在它的内部定义一些抽象的方法(供TemplateMethod调用的步骤)和一个TemplateMethod方法(非抽象方法),封装了这些抽象方法的抽象类(接口)就是骨架。而将它的实现延迟到子类中,也就是用子类实现它。不改变算法的结构而重新定义它的步骤,也就是改写或者实现父类的这些非TemplateMethod的抽象方法。   有时候,我们会遇到由一系列步骤构成的过程需要执行。这个过程从高层次上看是相同的,但有些步骤的实现可能不同。正如,查询SQL数据库从高层次上看过程是相同的,但某些细节比如如何连接数据库则可能因平台等细节的不同而不同。通过Template Method模式,我们可以先定义步骤序列,然后覆盖那些需要改变的步骤。   应用:   Template Method模式是一个使用频率比较高的模式,因为对于同一种类型的对象而言,他们之间一些处理流程往往是一致的,对象之间的差异仅在于具体的处理逻辑,因此,可以将通用的逻辑提取出来放到AbstractClass中实现,而将实现的具体细节交给子类完成。   从这一点上讲,Template Method与Strategy模式存在一定的相似性,但Template Method中实现的主体是ConcreteClass,AbstractClass仅定义了接口和希望子类重新定义的方法,通过继承来改变算法;而Strategy模式中Context类与Strategy类之间不存在继承关系,体现的是一种委托的关系。



1.1.23 Visitor访问者模式

  Visitor模式定义:表示一个作用于某对象结构中各元素的操作。它可以使你不修改各元素类的前提下定义作用于这些元素的新操作,也就是动态的增加新的方法。   Visitor模式的结构如下图所示:



图23-1 Visitor模式类图


  其中包括以下组成部分:   Visitor(访问者):为该对象结构中的每个ConcreteElement提供一个visit操作。该操作的名字和特征标识了要访问的具体元素角色,这样访问者就可以通过该元素的特定接口直接访问它。   ConcreteVisitor(具体访问者):实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片断乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态,这一状态常常在遍历该结构的过程中累积结果。   Element(元素):定义一个accept操作,它以一个访问者为参数,接受具体的访问者。   ConcreteElement(具体元素):实现Element的accept操作,该操作以一个访问者为参数。   ObjectStructure(对象结构,如Program):这是使用访问者模式必备的角色。能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。   下面代码按照类图的结构图来写的:

import java.util.ArrayList;
import java.util.Collection;
interface Visitor {//访问者
public void visitElementA(ConcreteElementA elementA);//针对具体元素A的新方法
public void visitElementB(ConcreteElementB elementB);//针对具体元素B的新方法
}
interface Element {//元素
public void accept(Visitor visitor);
}
class ConcreteVisitor implements Visitor {//具体的访问者
@Override
public void visitElementA(ConcreteElementA elementA) { //访问具体元素A
System.out.println(elementA.getName() + " visited by ConcreteVisitor ");
}
@Override
public void visitElementB(ConcreteElementB elementB) {
System.out.println(elementB.getName() + " visited by ConcreteVisitor ");
}
}
class ConcreteElementA implements Element {//具体元素A
private String name;
public ConcreteElementA(String name) {
this.name = name;
}
@Override
public void accept(Visitor visitor) {//接受访问者的访问:要把自己推送给访问者
visitor.visitElementA(this);
}
public String getName() {
return name;
}
}
class ConcreteElementB implements Element {//具体元素B
private String name;
public ConcreteElementB(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void accept(Visitor visitor) {//接受访问者的访问:要把自己推送给访问者
visitor.visitElementB(this);
}
}
class ObjectStructure {//对象结构:即元素的集合
private Collection collection = new ArrayList<>(); //维护一个元素列表
public void attach(Element element) {
collection.add(element);
}
public void detach(Element element) {
collection.remove(element);
}
public void accept(Visitor visitor) { //让每个元素者接受访问的访问
for (Element element : collection) {
element.accept(visitor);
}
}
}
public class VisitorClient {
public static void main(String args[]) {
Element elementA = new ConcreteElementA("ElementA");
Element elementB = new ConcreteElementB("ElementB");
Visitor visitor = new ConcreteVisitor();
ObjectStructure os = new ObjectStructure();
os.attach(elementA);
os.attach(elementB);
os.accept(visitor);
}
}  在上述实现中,我们可以发现,Visitor模式虽然使得为已有的类型添加新的抽象函数的需求变得容易实现,但是,Element类型与Visitor类型之间的耦合十分严重,出现了循环依赖,Visitor需要有所有Element子类的声明,而所有Element子类也需要包含Visitor类的头文件,当需要增加新的Element类型时,由于Visitor类的改动,将造成Element继承体系和Visitor继承体系全部需要重新编译。那么有什么办法来减轻耦合呢?在C++中,我们为每一个Element类型实现一个ConcreteVisitor,并最终通过多继承来实现IntegratedConcreteVisitor以解除这种耦合关系的实现方法,但这种实现方法使得继承体系变得更加复杂,同时还存在一些其它的开销。个人认为,Visitor模式是GoF所列举的23种模式中最复杂的,同时由于其使用上的约束较多,实际的应用并不太多。
  
Visitor模式一般在所谓的“双分派”问题中比较有用。
  Adapter模式告诉我们如何应对接口不一致对我们的设计造成的影响,但是,这并不能在如下的Context下发挥多大的作用:一个类系中的多个类要求支持相同的操作,但是这些类提供的接口并不一致。看到这里,你可能会说,我干嘛要用什么Adapter?我才没那么笨呢,我直接修改整个类系的接口方法,添加新的统一的接口方法不就OK了?
  确实如此,但是,如果你凑巧使用的是另一个不方便修改的模块的代码呢?在这种情况下,就可以使用Visitor(访问者)模式。
  Visitor模式用于表示一个作用于某对象结构中的各元素的操作,使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。Visitor模式借助所谓的“Double-Dispatch(双分派)”来达到以上目的。在C++、Java这样的典型的强类型、单分派语言中,我们通常使用的都是单分派,单分派的意思就是执行的方法是由调用者而不是调用参数决定的,譬如
  a.add(b)
  那么,这时执行的方法就是由a来决定的。add方法用于完成a + b操作,它需要根据a和b的不同类型执行不同的处理逻辑,这就需要双重分派。如果直接支持双分派的语言,那么执行这个方法就可以根据a和b两个的类型来决定。
  前面说过,C++、Java等并不能直接支持双分派,因此,要在C++、Java中支持所谓的双分派,必须通过增加额外的附加层和方法来实现。
  有两种主要的方式来实现Double-Dispatch,一种是type-switch。以下代码取自JDK:protected void processEvent(AWTEvent e) {
if (e instanceof FocusEvent) {
processFocusEvent((FocusEvent) e);
} else if (e instanceof MouseEvent) {
switch (e.getID()) {
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_ENTERED:
case MouseEvent.MOUSE_EXITED:
processMouseEvent((MouseEvent) e);
break;
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
processMouseMotionEvent((MouseEvent) e);
break;
case MouseEvent.MOUSE_WHEEL:
processMouseWheelEvent((MouseWheelEvent) e);
break;
}
} else if (e instanceof KeyEvent) {
processKeyEvent((KeyEvent) e);
} else if (e instanceof ComponentEvent) {
processComponentEvent((ComponentEvent) e);
} else if (e instanceof InputMethodEvent) {
processInputMethodEvent((InputMethodEvent) e);
} else if (e instanceof HierarchyEvent) {
switch (e.getID()) {
case HierarchyEvent.HIERARCHY_CHANGED:
processHierarchyEvent((HierarchyEvent) e);
break;
case HierarchyEvent.ANCESTOR_MOVED:
case HierarchyEvent.ANCESTOR_RESIZED:
processHierarchyBoundsEvent((HierarchyEvent) e);
break;
}
}
}  这种方式通过一堆的if-else,switch-case检查b的类型信息进行Re-Dispatch,虽然type-switch在设计上比较简单,但type-switch是OOD中应当尽量避免使用的技术,因为它可能给我们的代码引入一些难以察觉的Bug,以下面的代码为例(Java Code): class A {
}
class B extends A {
}
public class DispatchTest {
static public void main(String[] args) {
B b = new B();
if (b instanceof A) {
System.out.println("b is an instanceof A");
} else if (b instanceof B) {
System.out.println("b is an instanceof B");
}
}
}  程序运行的结果是:
bisaninstanceofA
  虽然从逻辑上讲,这个结论是正确的,但这显然不是我们期望的答案。要让上面的程序输出“b is an instanceof B”,需要调整上面的if判断的顺序,使子类判断出现在基类判断之前,由特殊到普通,否则,父类判断将屏蔽掉子类判断,对于简单的类型判断,使用type-switch是个不错的选择,但是当继承体系变得十分复杂时,判断顺序上的问题可能给你带来意想不到的麻烦(当然,还有别的办法,如通过getClass来检查类名信息,但这种方式比上面的方式也好不到哪里去)。
  还有一种Double-Dispatch实现方式在使用上相对较为安全,但实现较为复杂,而且需要更多的设计技巧,Visitor模式采用的是这一种形式。以下面的函数调用为例:
  a.add(Number b)
  我们可以在add函数体内采用type-switch方法对b进行判断,完成add操作,也可以像下面这样。对于整数类型,定义: public class Integer {
Number add(Number b) { //接受Number b的加动作,并把自己推送给b
return b.add(this);
}
//...
}  则不管b是什么类型,只要它实现了add(Integer a)这个方法,就可以准确完成add操作。这里的Number相当于Visitor,Integer相当于ConcreteElement,Integer是由Number来加(访问)的,它自己并没有去主动加b,而被b加了。
  对于浮点类型,定义: public class Float {
Number add(Number b) {
return b.add(this);
}
//...
}  则不管b是什么类型,只要它实现了add(Float a)这个方法,就可以准确完成add操作。
  可以看到,这时候把到底执行哪一个方法转交给了b,而且,在b执行该方法时,已经有了a的类型信息,因此,无需再进行type-switch。

  那么这种复杂的Double-Dispatch技术有什么好处呢?它的好处之一在于可以使我们在不改变a的同时,通过对b进行扩充,达到为a提供新的功能的目的。以上面的add为例,我们可以从Number派生出一种新的数值类型,在其中实现各种add操作,则可以在不改变已有数值类型的基础上与之协同工作。当然,由于在使用上存在一些限制,限制了Double-Dispatch的应用。


  关于双重分派,还可以参考我之前写的一篇文章,关于在C++中实现多态的双重分派:http://blog.csdn.net/zhoudaxia/article/details/4580438

  再举一个Visitor模式的例子: abstract class Parts {//Element角色
abstract void accept(Visitor visitor);
}
// component class: Wheel
class Wheel extends Parts {//ConcreteElement角色
private String name;
Wheel(String name) {
this.name = name;
}
String getName() {
return this.name;
}
@Override
void accept(Visitor visitor) { // function to support double-dispatch
visitor.visit(this);
}
}
// component class: Engine
class Engine extends Parts {
@Override
void accept(Visitor visitor) {
visitor.visit(this);
}
}
// component class: Body
class Body extends Parts {
@Override
void accept(Visitor visitor) {
visitor.visit(this);
}
}
// class to demonstrate visitor pattern and double-dispatch.
//If we don't use double-dispatch, we will lost all class info when we
//put all components into an array.
class Car { //ObjectStructure角色:管理对各元素的访问
private Parts[] parts = { new Engine(), new Body(), new Wheel("front left"),
new Wheel("front right"), new Wheel("back left"), new Wheel("back right") };
//把对各元素的访问委托给Visitor
void accept(Visitor visitor) {
visitor.visit(this);
for (int i = 0; i < parts.length; ++i) {
parts[i].accept(visitor);
}
}
}
// visitor interface, all concrete visitor class must implement it.
// need a access-function for each element class in the class-hierachy
interface Visitor {
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
// concrete visitor: PrintVisitor
class PrintVisitor implements Visitor { //具体的Visitor:完成对每个元素的访问
@Override
public void visit(Wheel wheel) {
System.out.println("Visiting " + wheel.getName()
+ " wheel");
}
@Override
public void visit(Engine engine) {
System.out.println("Visiting engine");
}
@Override
public void visit(Body body) {
System.out.println("Visiting body");
}
@Override
public void visit(Car car) {
System.out.println("Visiting car");
}
}
// more concrete visitor class, omitted...
// entry class
public class VisitorDemo {
static public void main(String[] args) {
Car car = new Car();
Visitor visitor = new PrintVisitor();
car.accept(visitor);
}
}  基本思想:
把访问对象中各元素(或者一个复合对象中的各原子对象)的工作委托给Visitor来完成。把各元素抽象一个元素类,它有一个接受访问请求的accept方法,此方法里面把真正的访问工作转发给Visitor来完成,并把自己的引用传过去。Visitor里面有各元素的访问方法,根据传来的引用访问该元素。由于一个对象有很多属性元素,故要有一个管理者ObjectStructure维护一个元素集合,并串起对各元素的访问。
  
在下列情况下可考虑使用Visitor模式:
  1、一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
  2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
  3、定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
  Visitor模式通过扩充新的继承体系来为已有的继承体系提供新的针对特殊类型的功能(达到与添加虚成员函数相同的效果),适用于十分稳定,并执行繁重处理的继承体系。
  
下面是访问者模式的一些优缺点:
  1、Visitor模式使得易于增加新的操作访问者,使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
  2、访问者集中相关的操作而分离无关的操作。相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数据结构都可以被隐藏在访问者中。
  3、增加新的ConcreteElement类很困难。Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Visitor中添加一个新的抽象操作,并在每一个ConcreteVisitor类中实现相应的操作。有时可以在Visitor中提供一个缺省的实现,这一实现可以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。
  所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象结构上的算法呢,还是构成该结构的各个对象的类。如果老是有新的ConcreteElement类加入进来的话,Visitor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一些。如果Element类层次是稳定的,而你不断地增加操作来修改算法,Visitor模式可以帮助你管理这些改动。
  4、通过类层次进行访问。一个迭代器可以通过调用节点对象的特定操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。
  5、累积状态。当访问者访问对象结构中的每一个元素时,它可能会累积状态。如果没有访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。
  6、破坏封装。访问者方法假定ConcreteElement接口的功能足够强,足以让访问者进行它们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它的封装性。

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台