访问者模式

来源:互联网 发布:淘宝手机链接生成 编辑:程序博客网 时间:2024/05/02 06:45

行为模式中的访问者模式

      访问者模式是对象的行为模式。访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。

动机:类层次结构中可能经常由于引入新的操作,从而将类型变得脆弱。。。在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有的设计。

如何在不更改类层次结构的前提下,在运行时根据需要透明地位层次结构上的各个类动态添加新的操作,从而避免上述问题?

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

双重分发:所谓双重分发即Visitor模式中间包括了两个多态分发(注意其中的多态机制):第一个为accept()方法的多态辨析;第二个visit方法的多态辨析。

 

问题:聚集是大多数系统都要处理的一种容器对象。它保存了对其它对象的引用。相信大家都有处理聚集的经验,但是大家处理过的大多数聚集恐怕都是同类对象的聚集。换言之,在聚集上采取的操作都是一些针对同类型对象的同类操作,而迭代子模式就是为这种情况准备的设计模式。

下面就是这样的一个例子,如代码清单1所示。

public void print(Collection collection) {

      

       for (Iterator iterator = collection.iterator(); iterator.hasNext();) {

           System.out.println(iterator.next());

       }

      

    }

那么一个很多人都没有考虑过的问题就是,如何针对一个保存有不同类型对象的聚集采取某种操作呢?

粗看上来,这并不是什么难题,仍然以上面的print()方法为例,如果collection聚集中的元素有可能还是聚集,那么调用聚集的toString就没有意义,应该调用它的内部元素的toString()方法。换言之,上面的方法就应当改写成下面的这样,如下代码:

 

 

public void print(Collection collection) {

      

       for (Iterator iterator = collection.iterator(); iterator.hasNext();) {

           Object o = iterator.next();

           if (Collection.class.isInstance(o)) {

              print((Collection)(o));

           } else {

              System.out.println(o.toString());

           }

       }

      

    }

 

但是这还没有完,如果这个操作对不同类型的元素有所不同时怎么办呢?比如系统可能会要求在打印字符串时,加上单引号;在打印Double类型的数据时,在数据后面加上”D”;在打印Float类型的数据时,在数据后面加上“F”。这时,直截了当的答案就是继续修改上面的print()方法,如代码清单:

public void print(Collection collection) {

      

       for (Iterator iterator = collection.iterator(); iterator.hasNext();) {

           Object o = iterator.next();

           if (Collection.class.isInstance(o)) {

              print((Collection)(o));

           } else if (o instanceof String) {

              System.out.println("/"" + o.toString() + "/"");

           } else if (o instanceof Double) {

              System.out.println(o.toString() + "D");

           } else if (o instanceof Float) {

              System.out.println(o.toString() + "F");

           } else {

              System.out.println(o.toString());

           }

       }

      

    }

这个条件转移语句变得越来越长,代码也越来越难以维护。换言之,如果需要针对一个包含不同类型元素的聚集采取某种操作,而操作的细节根据元素的类型不同而有所不同时,就会出现必须对元素类型判断的条件转移语句。这就是双重分派的实际应用。

这个时候,使用访问者模式就是一个值得考虑的解决方案。

访问者模式

访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。访问者模式的简略图如下图所示。

 

数据结构的每一个节点都可以接受一个访问者调用,此节点向访问者对象传入节点对象,而访问者对象则返回过来执行节点对象的操作,这样的过程叫做“双重分派”。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。

双重分派意味着是施加于节点之上的操作是基于访问者和节点本身的数据类型,而不仅仅是其中的一者。

访问者模式的结构

如下图所示,这个静态图显示了有两个具体访问者和两个具体节点的访问者模式的设计,必须指出的是,具体访问者的数目与具体节点的数目没有任何关系,虽然在这个示意图性的系统里面两者的数目都是两个。

 

访问者模式所涉及的角色:

访问者模式涉及到抽象访问者角色,具体访问者角色,抽象节点角色,具体节点角色,结构对象角色以及客户端角色。

     抽象访问者(Vistor)角色:说明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。

     具体访问者(ConcreteVisitor)角色:实现抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。

     抽象节点(Node)角色:声明一个接受操作,接受一个访问者对象作为一个参量。

     具体节点(Node)角色:实现了抽象元素所规定的接受操作。

     结构对象(ObjectStructure)角色:有如下的一些责任,可以遍历结构中的所有的元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如需要,可以设计成一个复合对象或者一个聚合,如列(List)或集合(Set

在实际系统中访问者模式通常是用来处理复杂的对象树结构的,而且访问者模式可以用来处理跨越多个等级结构的树结构的问题。这正是访问者模式的功能强大之处。

什么情况下应当使用访问者模式

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

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

显然,访问者模式提供了倾斜的可扩展性:方法集合的可扩展性和类集合的不可扩展性。

换言之,如果系统的数据结构时频繁变化的,则不适合使用访问者模式。

“开-闭”原则和对变化的封装

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

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

使用访问者模式的优缺点:

访问者模式使得增加新的操作更容易,如果一些操作依赖一个复杂的结构对象的话,那么一般而言,增加新的操作会很复杂,而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此变得更容易。

访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。

访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象,访问者模式可以做到这一点。

积累状态。每一个单独的访问者对象都集中了相关的行为,从而也就可以在访问过程中将执行的操作的状态积累在自己内部,而不是分散到很多的节点对象中,这是有益于系统维护的优点。