设计模式学习笔记--Visitor 访问者模式

  访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
  打个比喻,好像一棵树子(某种数据结构),以前上面只能挂一种果实,采用一种操作方法。而现在,上面既可以挂苹果,也可以挂梨子,甚至还可以挂香蕉(不同类型的对象),而我们的操作方法即可以用手摘,也可以用挂钩拉,还可以用钳子夹(作用于结构上的不同的操作),当然以后还可以包括其它任何我们想要采取的办法。那么应对此种变化,我们就引入了访问者模式。
   访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做"双重分派"。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。
   由上可以看到,双重分派是理解访问者模式的重点。而为了理解双重分派,我们要循序渐近地学习以下概念:
   1、静态分派   2、动态分派   3、单重分派  4、双重分派
 首先,我们的程序如下图:

         

 
  一、关于分派
   1、静态分派 
 

     StaticDispatch.cs代码

Code

  客户应用代码

Code

运行效果如下:

   2、动态分派

DynamicDispatch.cs代码如下:

Code

客户应用代码如下:

Code

运行效果如下:

  

3、单重分派

SingleDispatch.cs代码如下: 

Code

客户应用代码如下: 

Code

运行效果如下:
  

4、双重分派

DoubleDispatch.cs代码如下:

Code

客户应用代码如下:

Code

代码说明: 
            (a)关于face方法   (方法调用者的类型是运行期确定)
            尽管human声明为Animal类型,但human.face调用的是Human类中的override的face函数,这是因为动态分派只会体现在方法的调用者身上
            因为此处face方法的调用者是Human类型,虽然声明的是Animal类型,但new的却是Human类型,所以只会调用Human类中override的face方法
            因此显示了"动态分派"的特性
            (b)关于face方法中的参数  (方法参数的类型是编译期确定)
            接上面,我们知道实际调用的是Human类中的face方法,我们也知道在Human类中我们override了三个face方法,分别对应animal,human,cat三种类型的参数
            现在的问题是,此处我们定义了Animal cat = new Cat(),并把cat作为参数传入human.face(cat),那么,它将激发Human类中的哪一个override的face方法呢
            运行后,我们发现,是调用了public override void face(Animal animal)这个方法。这是因为方法的参数类型会在编译期由编译器决定,所以显示了"静态分派"的特性
            针对Animal cat = new Cat()虽然我们new 的是Cat类型,但声明的是Animal,所以在编译时对于human.face(cat)内的参数cat,类型其实确定为Animal,因此,传到了
            human.face方法中,调用的是public override void face(Animal animal)方法

            根据(a)(b),我们推断:如果参数cat的类型也能够在运行期决定,那么哪个face被调用就由方法调用者和方法参数共同在运行期决定了。
            那么如何实现参数类型在运行期绑定呢?既然方法调用者的类型是运行期才确定的,那么我们就可以反客为主了,将方法参数变成方法调用者。如下:
            face(Cat arg) {arg.face(this);}
            下面,我们把DoubleDispatch类中关于face方法定义代码段中的 //*****把此处解除注释实现双重分派***** 那一排的注释去掉就可以看到双重分派的效果。
            至此,我们应该明白了双重分派的涵义:哪个face最终被调用经过两次运行期类型绑定才确定下来,这样的过程就是双重分派了。进一步,自然可以理解多重分派。

运行效果如下:

二、进入访问者模式
 访问者模式的UML图如下:

        

 其相关角色如下:
 1、抽象访问者(Visitor)角色:声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
 2、具体访问者(ConcreteVisitor)角色:实现抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。
 3、抽象节点(Element)角色:声明一个接受操作,接受一个访问者对象作为一个参量。
 4、具体节点(ConcreteElement)角色:实现了抽象元素所规定的接受操作。
 5、结构对象(ObiectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。  
   (一)、访问者模式基本思路示例
  1、抽象访问者(Visitor)角色:

Code

  2、具体访问者(ConcreteVisitor)角色:

Code

  3、抽象节点(Element)角色:

Code

  4、具体节点(ConcreteElement)角色:

Code

  5、结构对象(ObiectStructure)角色:

Code

  6、客户应用代码

Code

  运行效果如下:

   (二)、雇员节点的访问者模式法例
  1、抽象访问者(Visitor)角色:IEmpVisitor

Code

  2、具体访问者(ConcreteVisitor)角色:EmpConcreteVisitors

Code

  3、抽象节点(Element)角色:EmpElement

Code

  4、具体节点(ConcreteElement)角色:EmpBaseElement--EmpConcreteElements
EmpBaseElement

Code

EmpConcreteElements

Code

  5、结构对象(ObiectStructure)角色:EmpStructure

Code

  6、客户应用代码

Code

 运行效果如下:

 
 总结

1、 在什么情况下应当使用访问者模式
有意思的是,在很多情况下不使用设计模式反而会得到一个较好的设计。换言之,每一个设计模式都有其不应当使用的情况。访问者模式也有其不应当使用的情况,让我们
先看一看访问者模式不应当在什么情况下使用。

倾斜的可扩展性

访问者模式仅应当在被访问的类结构非常稳定的情况下使用。换言之,系统很少出现需要加入新节点的情况。如果出现需要加入新节点的情况,那么就必须在每一个访问对象里加入一个对应于这个新节点的访问操作,而这是对一个系统的大规模修改,因而是违背"开一闭"原则的。

访问者模式允许在节点中加入新的方法,相应的仅仅需要在一个新的访问者类中加入此方法,而不需要在每一个访问者类中都加入此方法。

显然,访问者模式提供了倾斜的可扩展性设计:方法集合的可扩展性和类集合的不可扩展性。换言之,如果系统的数据结构是频繁变化的,则不适合使用访问者模式。

"开一闭"原则和对变化的封装

面向对象的设计原则中最重要的便是所谓的"开一闭"原则。一个软件系统的设计应当尽量做到对扩展开放,对修改关闭。达到这个原则的途径就是遵循"对变化的封装"的原则。这个原则讲的是在进行软件系统的设计时,应当设法找出一个软件系统中会变化的部分,将之封装起来。

很多系统可以按照算法和数据结构分开,也就是说一些对象含有算法,而另一些对象含有数据,接受算法的操作。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。

反过来,如果这样一个系统的数据结构对象易于变化,经常要有新的数据对象增加进来的话,就不适合使用访问者模式。因为在访问者模式中增加新的节点很困难,要涉及到在抽象访问者和所有的具体访问者中增加新的方法。
2、 使用访问者模式的优点和缺点
 访问者模式有如下的优点:

访问者模式使得增加新的操作变得很容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,增加新的操作会很复杂。而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此,变得很容易。
访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。
访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象。访问者模式可以做到这一点。
积累状态。每一个单独的访问者对象都集中了相关的行为,从而也就可以在访问的过程中将执行操作的状态积累在自己内部,而不是分散到很多的节点对象中。这是有益于系统维护的优点。
 访问者模式有如下的缺点:

增加新的节点类变得很困难。每增加一个新的节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作。
破坏封装。访问者模式要求访问者对象访问并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:它们必须暴露一些自己的操作和内部状态。不然,访问者的访问就变得没有意义。由于访问者对象自己会积累访问操作所需的状态,从而使这些状态不再存储在节点对象中,这也是破坏封装的。

 

前往:设计模式学习笔记清单
posted @ 2009-10-17 19:48  wsdj  阅读(999)  评论(0编辑  收藏  举报