JAVA设计模式之访问者模式详解

在阎宏博士的《JAVA与模式》一书中开头是这样描述访问者(Visitor)模式的:

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

分派的概念

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

复制代码 代码如下:

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

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

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

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

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

静态分派

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

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

复制代码 代码如下:

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,这破坏了对象的封装性。

时间: 2015-03-30

举例讲解设计模式中的访问者模式在Java编程中的运用

访问者(Visitor)模式:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作.访问者模式的结构图如下: 通过上图可以看到他有如下角色: 抽象访问者(Visitor)角色:定义接口,声明一个或多个访问操作. 具体访问者(ConcreteVisitor)角色:实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作. 抽象元素(Visitable)角色:声明一个接受操作,接受一个访问者对象作为一个参数. 具体元素结点(Concret

详解Java设计模式编程中的访问者模式

定义:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作. 类型:行为类模式 类图: 例子: 例如,思考一下添加不同类型商品的购物车,当点击结算的时候,它计算出所有不同商品需付的费用.现在,计算逻辑即为计算这些不同类型商品的价格.或者说通过访问者模式我们把此逻辑转移到了另外一个类上面.让我们实现这个访问者模式的例子. 为了实现访问者模式,最先需要做的是创建能够被添加到购物车中代表不同类型商品(itemElement)的类. ItemElement

Java设计模式之访问者模式使用场景及代码示例

Java设计模式访问者模式 模式概念 访问者模式表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作.访问者模式适用于数据结构相对稳定算法又易变化的系统,若系统数据结构对象易于变化,则不适合使用访问者模式.访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者. Visitor应用场景 一定会有的疑问:visitor和iterator的区别: visitor可以访问不同的对象(只需要在Element定义对应的accept),但是Ite

JAVA设计模式之访问者模式原理与用法详解

本文实例讲述了JAVA设计模式之访问者模式.分享给大家供大家参考,具体如下: 访问者模式: 一个作用于某对象结构中各元素的操作,使你可以在不改变各元素类数据结构的前提下增加作用于这些元素的新操作. 结构对象是访问者模式必备条件,且这个结构对象必须存在遍历自身各个对象的方法. 适用于:数据结构相对稳定,把数据结构和作用与其上的其它操作解耦,使得操作相对自由. 优点: 1.符合单一职责原则 2.扩展性良好:元素类可以通过接受不同的访问者来实现对不同操作的扩展. 缺点: 1.如果要增加新元素,则会让操

JAVA设计模式之建造者模式原理与用法详解

本文实例讲述了JAVA设计模式之建造者模式定义与用法.分享给大家供大家参考,具体如下: 建造者模式:将复杂对象的构造与它的实现相分离,让相同的构建过程可以创建不同的对象. 适用场合: 复杂对象的算法应该独立于该对象的组成部分或当构造过程允许被构造不同的对象时. 组成角色: 1 创建者(Builder)接口:为创建一个对象的对应部件所指定抽象接口. 2 具体创建者(ConcreteBuilder):实现Builder的接口以构造对象的各个部件. 3 具体创建者管理对象(Director):使用Bu

JAVA设计模式之组合模式原理与用法详解

本文实例讲述了JAVA设计模式之组合模式.分享给大家供大家参考,具体如下: 组合(整体与部分关系)模式:将不同但是相关的对象组合成树形结构以实现"部分-整体"的层次结构,使得用户对单个对象和组合对象的使用具有一致性. * 模式角色组成: 1.Component对象: 是组合中的对象接口,是所有类共有的接口.是用于统一定义整体中的部分. 2.Leaf对象: 整体中的部分,没有下一级. 3.Composite对象: 用来存储子部件,在Component接口中实现与部分有关操作. 以公司构成

JAVA设计模式之备忘录模式原理与用法详解

本文实例讲述了JAVA设计模式之备忘录模式.分享给大家供大家参考,具体如下: 备忘录模式:又叫做快照模式,指在不破坏封装性的前提下,获取到一个对象的内部状态,并在对象之外记录或保存这个状态.在有需要的时候可将该对象恢复到原先保存的状态.我们相当于把对象原始状备份保留,所以叫备忘录模式. *模式 角色对象组成: 1.发起者对象:负责创建一个备忘录来记录当前对象的内部状态,并可使用备忘录恢复内部状态. 2.备忘录对象:负责存储发起者对象的内部状态,并防止其他对象访问备忘录. 3.管理者对象:负责备忘

Java设计模式之策略模式定义与用法详解

本文实例讲述了Java策略模式定义与用法.分享给大家供大家参考,具体如下: 一. 定义: 定义一系列算法,把他们一个一个封装起来,并且使他们可以相互替换. 二. 优点: (1)上下文(Context)和具体策略(ConcreteStrategy)是松耦合关系,因此上下文只需要知道他要使用某一个实现  Strategy接口类的实例,但不需要知道是哪个类. (2)策略模式满足开闭原则,当增加新的具体类时,不需要修改上下文类的代码,上下文即可以引用新的具体策略的实例. 三. 实例: 下面就通过一个问题

Python设计模式之备忘录模式原理与用法详解

本文实例讲述了Python设计模式之备忘录模式原理与用法.分享给大家供大家参考,具体如下: 备忘录模式(Memento Pattern):不破坏封装性的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,这样已经后就可将该对象恢复到原先保存的状态 下面是一个备忘录模式的demo: #!/usr/bin/env python # -*- coding:utf-8 -*- __author__ = 'Andy' """ 大话设计模式 设计模式--备忘录模式 备忘录模式(Me

Python设计模式之状态模式原理与用法详解

本文实例讲述了Python设计模式之状态模式原理与用法.分享给大家供大家参考,具体如下: 状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类 下面是一个状态模式的demo: #!/usr/bin/env python # -*- coding:utf-8 -*- __author__ = 'Andy' """ 大话设计模式 设计模式--状态模式 状态模式(State Pattern):当一个对象的内在状态改变时允许改

Java经典设计模式之策略模式原理与用法详解

本文实例讲述了Java经典设计模式之策略模式.分享给大家供大家参考,具体如下: 策略模式指:策略模式指将程序中可变部分抽象分离成一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模式让算法独立于使用它的客户而独立变化. 策略模式一般由下面三部分组成: 1. 抽象策略角色: 策略类,通常由一个接口或者抽象类实现. 2. 具体策略角色:包装了相关的算法和行为. 3. 环境角色:持有某一个策略类的引用,客户端调用. 策略模式设计原则: 1. 把程序中需要变化的部分抽离出来,独立于不变

Java设计模式之策略模式原理与用法实例详解

本文实例讲述了Java设计模式之策略模式原理与用法.分享给大家供大家参考,具体如下: 策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模式让算法独立于使用它的客户而独立变化.其中JDK里面的TreeSet类和TreeMap类就用到了策略模式.这两个类是带排序的集合类,其中排序的规则就相当于策略模式里定义的一系列算法,而集合类就相当于是策略模式里的环境类,供用户使用,用只知道TreeSet和TreeMap是带排序的,至于怎么排序的,是由排序的算法决定的. 策略模式

PHP设计模式之状态模式定义与用法详解

本文实例讲述了PHP设计模式之状态模式定义与用法.分享给大家供大家参考,具体如下: 什么是状态设计模式 当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类. 状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况.把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化. 什么时候使用状态模式 对象中频繁改变非常依赖于条件语句. 就其自身来说, 条件语句本身没有什么问题(如switch语句或带else子句的语句),不过, 如果选项太多, 以到程序开