访问者(Visitor)模式

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

分派的概念

  变量被声明时的类型叫作变量的静态类型(Static Type),有些人又把静态类型叫作明显类型(Apparent Type);而变量所引用的对象的真实类型又叫作变量的实际类型(Actual Type)。好比:算法

List list = null;
list = new ArrayList();

  声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。设计模式

  根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派动态分派数据结构

  静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于咱们来讲并不陌生,方法重载就是静态分派。ide

  动态分派(Dynamic Dispatch)发生在运行时期,动态分派动态地置换掉某个方法。测试

 静态分派

  Java经过方法重载支持静态分派。用墨子骑马的故事做为例子,墨子能够骑白马或者黑马。墨子与白马、黑马和马的类图以下所示:this

  在这个系统中,墨子由Mozi类表明spa

public class Mozi {
    
    public void ride(Horse h){
        System.out.println("骑马");
    }
    
    public void ride(WhiteHorse wh){
        System.out.println("骑白马");
    }
    
    public void ride(BlackHorse bh){
        System.out.println("骑黑马");
    }
    
    public static void main(String[] args) {
        Horse wh = new WhiteHorse();
        Horse bh = new BlackHorse();
        Mozi mozi = new Mozi();
        mozi.ride(wh);
        mozi.ride(bh);
    }

}

  显然,Mozi类的ride()方法是由三个方法重载而成的。这三个方法分别接受马(Horse)、白马(WhiteHorse)、黑马(BlackHorse)等类型的参数。设计

  那么在运行时,程序会打印出什么结果呢?结果是程序会打印出相同的两行“骑马”。换言之,墨子发现他所骑的都是马。对象

  为何呢?两次对ride()方法的调用传入的是不一样的参数,也就是wh和bh。它们虽然具备不一样的真实类型,可是它们的静态类型都是同样的,均是Horse类型。

  重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

 动态分派

  Java经过方法的重写支持动态分派。用马吃草的故事做为例子,代码以下所示:

public class Horse {
    
    public void eat(){
        System.out.println("马吃草");
    }
}
public class BlackHorse extends Horse {
    
    @Override
    public void eat() {
        System.out.println("黑马吃草");
    }
}
public class Client {

    public static void main(String[] args) {
        Horse h = new BlackHorse();
        h.eat();
    }

}

  变量h的静态类型是Horse,而真实类型是BlackHorse。若是上面最后一行的eat()方法调用的是BlackHorse类的 eat()方法,那么上面打印的就是“黑马吃草”;相反,若是上面的eat()方法调用的是Horse类的eat()方法,那么打印的就是“马吃草”。

  因此,问题的核心就是Java编译器在编译时期并不老是知道哪些代码会被执行,由于编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而 方法的调用则是根据对象的真实类型,而不是静态类型。这样一来,上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,打印的是 “黑马吃草”。

 分派的类型

  一个方法所属的对象叫作方法的接收者,方法的接收者与方法的参数统称作方法的宗量。好比下面例子中的Test类

public class Test {

    public void print(String str){
        System.out.println(str);
    }
}

  在上面的类中,print()方法属于Test对象,因此它的接收者也就是Test对象了。print()方法有一个参数是str,它的类型是String。

  根据分派能够基于多少种宗量,能够将面向对象的语言划分为单分派语言(Uni-Dispatch)和多分派语言(Multi-Dispatch)。单分派语言根据一个宗量的类型进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。

  C++和Java均是单分派语言,多分派语言的例子包括CLOS和Cecil。按照这样的区分,Java就是动态的单分派语言,由于这种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,由于这种语言对重载方法的分派会考虑到方法的接收者的类型以及方法的全部参数的类型。

  在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪个操做:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择 过程,使得只有一个宗量能够被考虑到,这个宗量一般就是方法的接收者。在Java语言里面,若是一个操做是做用于某个类型不明的对象上面,那么对这个对象 的真实类型测试仅会发生一次,这就是动态的单分派的特征。

 双重分派

  一个方法根据两个宗量的类型来决定执行不一样的代码,这就是“双重分派”。Java语言不支持动态的多分派,也就意味着Java不支持动态的双分派。可是经过使用设计模式,也能够在Java语言里实现动态的双重分派。

  在Java中能够经过两次方法调用来达到两次分派的目的。类图以下所示:

  在图中有两个对象,左边的叫作West,右边的叫作East。如今West对象首先调用East对象的goEast()方法,并将它本身传入。 在East对象被调用时,当即根据传入的参数知道了调用者是谁,因而反过来调用“调用者”对象的goWest()方法。经过两次调用将程序控制权轮番交给 两个对象,其时序图以下所示:

  这样就出现了两次方法调用,程序控制权被两个对象像传球同样,首先由West对象传给了East对象,而后又被返传给了West对象。

  可是仅仅返传了一下球,并不能解决双重分派的问题。关键是怎样利用这两次调用,以及Java语言的动态单分派功能,使得在这种传球的过程当中,可以触发两次单分派。

  动态单分派在Java语言中是在子类重写父类的方法时发生的。换言之,West和East都必须分别置身于本身的类型等级结构中,以下图所示:

  源代码

  West类

public abstract class West {
    
    public abstract void goWest1(SubEast1 east);
    
    public abstract void goWest2(SubEast2 east);
}

  SubWest1类

public class SubWest1 extends West{
    
    @Override
    public void goWest1(SubEast1 east) {
        
        System.out.println("SubWest1 + " + east.myName1());
    }
    
    @Override
    public void goWest2(SubEast2 east) {
        
        System.out.println("SubWest1 + " + east.myName2());
    }
}

  SubWest2类

public class SubWest2 extends West{
    @Override
    public void goWest1(SubEast1 east) {
        
        System.out.println("SubWest2 + " + east.myName1());
    }
    
    @Override
    public void goWest2(SubEast2 east) {
        
        System.out.println("SubWest2 + " + east.myName2());
    }
}

  East类

public abstract class East {

    public abstract void goEast(West west);
}

  SubEast1类

public class SubEast1 extends East{
    @Override
    public void goEast(West west) {
        west.goWest1(this);
    }
    
    public String myName1(){
        return "SubEast1";
    }
}

  SubEast2类

public class SubEast2 extends East{
    @Override
    public void goEast(West west) {
        west.goWest2(this);
    }
    
    public String myName2(){
        return "SubEast2";
    }
}

  客户端类

public class Client {

    public static void main(String[] args) {
        //组合1
        East east = new SubEast1();
        West west = new SubWest1();
        east.goEast(west);
        //组合2
        east = new SubEast1();
        west = new SubWest2();
        east.goEast(west);
    }

}

  运行结果以下

SubWest1 + SubEast1
SubWest2 + SubEast1

  系统运行时,会首先建立SubWest1和SubEast1对象,而后客户端调用SubEast1的goEast()方法,并将SubWest1对象传入。因为SubEast1对象重写了其超类East的goEast()方法,所以,这个时候就发生了一次动态的单分派。当SubEast1对象接到调用时,会从参数中获得SubWest1对象,因此它就当即调用这个对象的goWest1()方法,并将本身传入。因为SubEast1对象有权选择调用哪个对象,所以,在此时又进行一次动态的方法分派。

  这个时候SubWest1对象就获得了SubEast1对象。经过调用这个对象myName1()方法,就能够打印出本身的名字和SubEast对象的名字,其时序图以下所示:

  因为这两个名字一个来自East等级结构,另外一个来自West等级结构中,所以,它们的组合式是动态决定的。这就是动态双重分派的实现机制。

访问者模式的结构

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

  数据结构的每个节点均可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操做。这样的过程叫作“双重分派”。节点调用访问者,将它本身传入,访问者则将某算法针对此节点执行。访问者模式的示意性类图以下所示:

  

  访问者模式涉及到的角色以下:

  ●  抽象访问者(Visitor)角色:声明了一个或者多个方法操做,造成全部的具体访问者角色必须实现的接口。

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

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

  ●  具体节点(ConcreteNode)角色:实现了抽象节点所规定的接受操做。

  ●  结构对象(ObjectStructure)角色:有以下的责任,能够遍历结构中的全部元素;若是须要,提供一个高层次的接口让访问者对象能够访问每个元素;若是须要,能够设计成一个复合对象或者一个汇集,如List或Set。

  源代码

  能够看到,抽象访问者角色为每个具体节点都准备了一个访问操做。因为有两个节点,所以,对应就有两个访问操做。

public interface Visitor {
    /**
     * 对应于NodeA的访问操做
     */
    public void visit(NodeA node);
    /**
     * 对应于NodeB的访问操做
     */
    public void visit(NodeB node);
}

  具体访问者VisitorA类

public class VisitorA implements Visitor {
    /**
     * 对应于NodeA的访问操做
     */
    @Override
    public void visit(NodeA node) {
        System.out.println(node.operationA());
    }
    /**
     * 对应于NodeB的访问操做
     */
    @Override
    public void visit(NodeB node) {
        System.out.println(node.operationB());
    }

}

  具体访问者VisitorB类

public class VisitorB implements Visitor {
    /**
     * 对应于NodeA的访问操做
     */
    @Override
    public void visit(NodeA node) {
        System.out.println(node.operationA());
    }
    /**
     * 对应于NodeB的访问操做
     */
    @Override
    public void visit(NodeB node) {
        System.out.println(node.operationB());
    }

}

  抽象节点类

public abstract class Node {
    /**
     * 接受操做
     */
    public abstract void accept(Visitor visitor);
}

  具体节点类NodeA

public class NodeA extends Node{
    /**
     * 接受操做
     */
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    /**
     * NodeA特有的方法
     */
    public String operationA(){
        return "NodeA";
    }

}

  具体节点类NodeB

public class NodeB extends Node{
    /**
     * 接受方法
     */
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    /**
     * NodeB特有的方法
     */
    public String operationB(){
        return "NodeB";
    }
}

  结构对象角色类,这个结构对象角色持有一个汇集,并向外界提供add()方法做为对汇集的管理操做。经过调用这个方法,能够动态地增长一个新的节点。

public class ObjectStructure {
    
    private List<Node> nodes = new ArrayList<Node>();
    
    /**
     * 执行方法操做
     */
    public void action(Visitor visitor){
        
        for(Node node : nodes)
        {
            node.accept(visitor);
        }
        
    }
    /**
     * 添加一个新元素
     */
    public void add(Node node){
        nodes.add(node);
    }
}

  客户端类

public class Client {

    public static void main(String[] args) {
        //建立一个结构对象
        ObjectStructure os = new ObjectStructure();
        //给结构增长一个节点
        os.add(new NodeA());
        //给结构增长一个节点
        os.add(new NodeB());
        //建立一个访问者
        Visitor visitor = new VisitorA();
        os.action(visitor);
    }

}

  虽然在这个示意性的实现里并无出现一个复杂的具备多个树枝节点的对象树结构,可是,在实际系统中访问者模式一般是用来处理复杂的对象树结构的,并且访问者模式能够用来处理跨越多个等级结构的树结构问题。这正是访问者模式的功能强大之处。

  准备过程时序图

  首先,这个示意性的客户端建立了一个结构对象,而后将一个新的NodeA对象和一个新的NodeB对象传入。

  其次,客户端建立了一个VisitorA对象,并将此对象传给结构对象。

  而后,客户端调用结构对象汇集管理方法,将NodeA和NodeB节点加入到结构对象中去。

  最后,客户端调用结构对象的行动方法action(),启动访问过程。

  

  访问过程时序图

  

  结构对象会遍历它本身所保存的汇集中的全部节点,在本系统中就是节点NodeA和NodeB。首先NodeA会被访问到,这个访问是由如下的操做组成的:

  (1)NodeA对象的接受方法accept()被调用,并将VisitorA对象自己传入;

  (2)NodeA对象反过来调用VisitorA对象的访问方法,并将NodeA对象自己传入;

  (3)VisitorA对象调用NodeA对象的特有方法operationA()。

  从而就完成了双重分派过程,接着,NodeB会被访问,这个访问的过程和NodeA被访问的过程是同样的,这里再也不叙述。

访问者模式的优势

  ●  好的扩展性

  可以在不修改对象结构中的元素的状况下,为对象结构中的元素添加新的功能。

  ●  好的复用性

  能够经过访问者来定义整个对象结构通用的功能,从而提升复用程度。

  ●  分离无关行为

  能够经过访问者来分离无关的行为,把相关的行为封装在一块儿,构成一个访问者,这样每个访问者的功能都比较单一。

访问者模式的缺点

  ●  对象结构变化很困难

  不适用于对象结构中的类常常变化的状况,由于对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价过高。

  ●  破坏封装

  访问者模式一般须要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性。

相关文章
相关标签/搜索