访问者模式

来源:互联网 发布:智能电视广告屏蔽软件 编辑:程序博客网 时间:2024/05/01 23:20
 
访问者模式(Visitor Pattern)
     访问者模式是对象的行为模式。访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
一、问题
     集合是大多数的系统都要处理的一种容器对象,它保存了对其它对象的引用。一般情况下,在集合上采取的操作都是针对同类型对象的同类操作,而迭代子模式就是为这种情况准备的设计模式。
     但是,任何针对一个保存有不同类型对象的集合采取某种操作呢?
     如果需要针对一个包含不同类型元素的集合采取某种操作,而操作的细节根据元素的类型不同而有所不同时,就会出现必须对元素类型做类型判断的条件转移语句。而这些条件判断语句会写的很长,不利于代码的维护。这个时候,使用访问者模式是一个值得考虑的方案。
二、访问者模式
     访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构之上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。
     数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,二访问者对象则反过来执行节点对象的操作。这样的过程叫做“双重分派”。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。
     双重分派意味着施加于节点之上的操作是基于访问者和节点本身的数据类型,而不仅是其中的一者。
三、访问者模式的结构
1、访问者模式涉及的角色
     (1)抽象访问者角色(Visitor):声明了一个或者多个访问操作,形成所有的集体元素角色必须实现的接口
     (2)具体访问者角色(ConcreteVisitor):实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作。
     (3)抽象节点角色(Node):声明一个接受操作,接受一个访问者对象作为一个参量。
     (4)具体节点角色(Node):实现了抽象元素所规定的接受操作。
     (5)结构对象角色(ObjectStructure):可以遍历结构中所有的元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或一个集合,如(List)或(Set)。
2、示意性源代码
     (1)抽象访问者角色:为每个节点都提供了一个访问操作,接收相应的节点对象作为参量。
public interface Visitor
{
    //访问节点A的操作
     void visit(NodeA node);
     //访问节点B的操作
    void visit(NodeB node);
}
     (2)具体访问者角色:
public class VisitorA implements Visitor
{
    public void visit(NodeA nodeA)
    {
        System.out.println( nodeA.operationA() );
    }
    public void visit(NodeB nodeB)
    {
        System.out.println( nodeB.operationB() );
    }
}
    
public class VisitorB implements Visitor
{
    public void visit(NodeA nodeA)
    {
        System.out.println( nodeA.operationA() );
    }
 
    public void visit(NodeB nodeB)
    {
        System.out.println( nodeB.operationB() );
    }
}
     (3)抽象节点角色:声明了一个接受操作
abstract public class Node
{
    public abstract void accept(Visitor visitor);
}
     (4)具体节点角色:接受一个访问者对象作为参量,同时还有若干个商业方法
public class NodeA extends Node
{
    public void accept(Visitor visitor)
    {
        visitor.visit(this);
    }
 
    public String operationA()
    {
        return "NodeA is visited";
    }
}
 
public class NodeB extends Node
{
    public void accept(Visitor visitor)
    {
        visitor.visit(this);
    }
 
    public String operationB()
    {
       return "NodeB is visited";
    }
}
     (5)结构对象角色:持有一个集合,并向外界提供add()方法作为对集合的管理操作。通过调用这个方法,可以动态的增加一个新的节点。
public class ObjectStructure
{
    private Vector nodes;
    private Node node;
    //构造方法
     public ObjectStructure()
    {
        nodes = new Vector();
    }
    //执行访问操作
     public void action(Visitor visitor)
    {
        for(Enumeration e = nodes.elements();e.hasMoreElements();)
        {
            node = (Node)e.nextElement();
            node.accept(visitor);
        }
    }
    //增加一个新元素
     public void add(Node node)
    {
        nodes.addElement(node);
    }
}
     (6)客户端
public class Client
{
    private static ObjectStructure aObjects;
    private static Visitor visitor;
    static public void main(String[] args)
    {
        //创建一个结构对象
         aObjects = new ObjectStructure();
         //给结构增加一个节点
        aObjects.add(new NodeA());
         aObjects.add(new NodeB());
         //创建一个新的访问者
        visitor = new VisitorA();
        //让访问者访问节点
         aObjects.action(visitor);
    }
}
     在实际系统中访问者模式通常用来处理复杂的对象树结构,而且访问者模式可以用来处理跨越多个等级结构的树结构问题。这正是访问者模式的功能强大之处。
3、系统的时序
     (1)准备过程
         首先,客户端创建了一个结构对象,然后一个新的节点对象A和B传入。
         其次,客户端创建了一个访问者对象,并将此对象传给结构对象。
         然后,客户端调用结构对象集合管理方法,将节点A和B加入到结构对象中。
         最后,客户端调用结构对象的行动方法,启动访问过程。
     (2)访问过程
         结构对象会遍历它自己所保存的集合中的所以节点。在本系统中就是节点A和B,首先A会被访问到,这个访问由以下操作组成
1节点A对象的接受方法被调用,并将访问者A对象本身传入;
2节点A反过来调用访问者A对象的访问方法,并将节点A对象本身传入;
3访问者A对象调用节点A对象的商业方法operaitonA()。
     从而完成了双重分派过程,接着节点B对象被访问,访问过程和节点A的过程一样。
     因此,结构对象对集合元素的遍历过程就是对集合中所有的节点进行委派的过程,也就是双重分派过程,系统由多少个节点就会发生多少个双重分派。
四、在什么情况下应当使用访问模式
     在很多情况下不使用设计模式反而会得到一个比较好的设计。每一个设计模式都有其不应当使用的地方。访问者模式也有不应当使用的情况。
1、倾斜的扩展性
     访问者模式仅当在被访问的类结构非常稳定的情况下使用。换言之,系统很少出现需要加入新节点的情况。如果出现需要加入新节点的情况怎么办呢?那时就必须在每个访问对象里加入一个对应于这个新节点的访问操作,而这是对系统的大规模修改,因而是违背“开-闭”原则的。
     访问者模式允许在节点中加入新的方法,相应的仅仅需要在一个新的访问者类加入此方法,而不需要在每一个访问者类中都加入此方法。
     显然,访问者模式提供了倾斜的可扩展设计:方法集合的可扩展性和类集合的不可扩展性。
     换言之,如果系统的数据结构是频繁变化的,则不适合使用访问者模式。
2、“开—闭”原则和对变化的封装
     面向对象的设计原则只最重要的便是所谓的“开—闭”原则。一个软件系统的设计应当尽量做到对扩展开放,对修改关闭。达到这个原则的途径就是遵循“对变化的封装”的原则。即在进行软件系统的设计时,应当设法找出一个软件系统中会变化的部分,将之封装起来。
     很多的系统可以按照算法和数据结构分开,也就是说一些对象含有算法,而另一些对象含有数据,接受算法的操作。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者没收就是比较合适的。因为访问者没收使得算法操作的增加变得容易。
     反过来,如果一个系统的数据结构对象易于变化,经常要有新的数据对象增加进来的话,就不适合使用访问者没收。因为在访问者没收中增加新的节点很困难,要涉及到在抽象访问者和所有的具体访问者中增加新的方法。
五、访问者模式的优缺点
1、优点
     (1)访问者模式使得增加新的操作变得很容易。如果一些操作依赖于一个复杂得结构对象的话,那么一般而言,增加新的操作会很复杂。而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此,变的很容易。
     (2)访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个节点类中。
     (3)访问者模式可以跨越几个类得等级结构访问属于不同的等级结构的成员类。迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象。访问者模式可以做到这一点。
2、缺点
     (1)增加新的节点变的很困难。每增加一个新的节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作。
     (2)破坏封装。访问者模式要求访问者对象访问者并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:它们必须暴露一些自己的操作和内部状态。不然,访问者的访问者就变得没有意义。由于访问者对象自己会积累访问者操作所需的状态,从而使这些状态不再存储在节点对象中,这也是破坏封装的。
六、访问者模式的实现
     当实现访问者模式时,要将尽可能多的对象浏览逻辑放在Visitor类中。而不是放在它的子类中。这样的话,ConcreteVisitor类所访问的对象结构依赖较少,从而使维护较为容易。
1、对节点结构的优化
     访问者模式所涉及的接受访问的结构对象可以属于一个等级结构,也可以属于不同的等级结构。对于每一个等级结构而言,都可以通过将行为向上移动,将状态向下移动而加以优化。代码的优化方向与状态的优化方向恰好相反。
2、谁负责遍历行为
     由于每一个访问者都需要遍历结构对象只的每一个元素,因此,一个自然的问题就是,哪一个角色负责遍历行为?可供选择的有:结构对象、访问者对象或者创建一个新的迭代对象。
     (1)由结构对象负责迭代是最常见的选择,这也是需要结构角色的出发点。
     (2)另一个解决方案是使用一个迭代对象来负责遍历行为。
     (3)将遍历行为放到访问者中是第三个可能的选择。一般而言,这样做会导致每一个具体访问者都不得不具有管理集合的内部功能,而这在一般情况下是不理想的。然如果遍历的逻辑较为复杂的话,将所有的遍历逻辑放到结构角色中,不如将逻辑放到各个具体访问者角色中。
3、是否需要结构对象角色
     如果遍历行为是放到具体访问者中,那么结构对象角色就可以省略。
4、是否需要抽象访问者角色
     抽象访问者角色的设置是为了将可以复用的部分放在抽象类中,以便具体访问者角色可以继承,从而达到复用的目的。如果设计师非常肯定访问者只有一个,那么设置抽象访问者角色便没有太大的意义,可以省略。