java/android 设计模式学习笔记(24)---访问者模式

Posted Shawn_Dut

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java/android 设计模式学习笔记(24)---访问者模式相关的知识,希望对你有一定的参考价值。

  这篇博客我们来介绍访问者模式(Visitor Pattern),这也是行为型设计模式之一。访问者模式是一种将数据操作与数据结构分离的设计模式,它可以算是 23 中设计模式中最复杂的一个,但它的使用频率并不是很高,大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是需要使用它了。
  访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。
  转载请注明出处:http://blog.csdn.net/self_study/article/details/52778713
  PS:对技术感兴趣的同鞋加群544645972一起交流。

设计模式总目录

  java/android 设计模式学习笔记目录

特点

  封装一些作用于某种数据结构中的个元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
  访问者模式使用的场景

  • 对象结构比较稳定,但经常需要在此对象结构上定义新的操作;
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

UML类图

  
  访问者模式角色介绍:

  • Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数是一样的,因此访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式;
  • ConcreteVisitor:具体地访问者,它需要给出对每一个元素类访问时所产生的具体行为;
  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问;
  • ElementA,ElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法;
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。
  我们在介绍双重分派的时候会列出这个模式的通用代码。

示例与源码

  查阅相关资料时,了解到了有一个分派的概念,总结一下:

分派的概念

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

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

声明了一个变量list,它的静态类型(也叫明显类型)是List,而它的实际类型是ArrayList。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派动态分派静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派;动态分派(Dynamic Dispatch)发生在运行时期,动态分派动态地置换掉某个方法。接着我们来具体看看静态分派动态分派

静态分派

  Java通过方法重载支持静态分派,我们以一个例子来看一下:

public class StaticDispatch 

    public void sayHello(Human guy) 
        System.out.println("hello, guy!");
    

    public void sayHello(Man guy) 
        System.out.println("hello, man!");
    

    public void sayHello(Women guy) 
        System.out.println("hello, women!");
    

    public static void main(String[] args) 

        Human man = new Man();

        Human women = new Women();

        StaticDispatch sd = new StaticDispatch();

        sd.sayHello(man);

        sd.sayHello(women);

    


class Human 


class Man extends Human 


class Women extends Human 

最后的输出结果:

hello, guy!
hello, guy!

没错,程序就是大家熟悉的重载(Overload),而且大家也应该能知道输出结果,但是为什么输出结果会是这个呢,先来看一下代码的定义:

Human man = new Man();

我们把 Human 称为变量的 静态类型, Man 称为变量的 实际类型,其中,变量的静态类型和动态类型在程序中都可以发生变化,而区别是变量的静态类型是在编译阶段就可知的,但是动态类型要在运行期才可以确定,编译器在编译的时候并不知道变量的实际类型是什么。现在回到代码中,由于方法的接受者已经确定是 StaticDispatch 的实例 sd 了,所以最终调用的是哪个重载版本也就取决于传入参数的类型了。实际上,虚拟机(应该说是编译器)在重载时时通过参数的静态类型来当判定依据的,而且静态类型在编译期就可知,所以编译器在编译阶段就可根据静态类型来判定究竟使用哪个重载版本。于是对于例子中的两个方法的调用都是以 Human 为参数的版本,Java中,所有以静态类型来定位方法执行版本的分派动作,都称为静态分派

动态分派

  我们再来看看动态分派,Java 通过方法的重写支持动态分派,它和多态的另外一个重要体现有很大的关联,这个体现是什么,可能大家也能猜出,没错,就是重写(override),我们来看看例子:

public class DynamicDispatch 

    public static void main(String[] args) 
        Human man = new Man();
        Human women = new Women();

        man.sayHello();
        women.sayHello();

        man = new Women();
        man.sayHello();
    



abstract class Human 
    protected abstract void sayHello();


class Man extends Human 
    @Override
    protected void sayHello() 
        System.out.println("hello man!");
    


class Women extends Human 
    @Override
    protected void sayHello() 
        System.out.println("hello women!");
    

输出结果很显然:

hello man!
hello women!
hello women!

其实由两次改变 man 变量的实际类型导致调用函数版本不同,我们就可以知道,虚拟机是根据变量的实际类型来调用重写方法的。我们也可以从例子中看出,变量的实际类型是在运行期确定的,重写方法的调用也是根据实际类型来调用的,而不是根据静态类型。我们把这种在运行期根据实际类型来确定方法执行版本的分派动作,称为动态分派。

分派的类型

  一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参数统称做方法的宗量。比如下面例子中的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 都必须分别置身于自己的类型等级结构中,就正如上面的访问者模式 uml 类图,我们写出访问者模式的通用代码:
Visitor角色:West.class

public abstract class West 

    public abstract void goWest1(SubEast1 east);
    public abstract void goWest2(SubEast2 east);

ConcreteVisitor角色:SubWest1.class 和 SubWest2.class

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());
    
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());
    

Element角色:East.class

public abstract class East 
    public abstract void goEast(West west);

ConcreteElement角色:SubEast1.class 和 SubEast2.class

public class SubEast1 extends East
    @Override
    public void goEast(West west) 
        west.goWest1(this);
    

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

    public String myName2()
        return "SubEast2";
    

Client.class

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等级结构中,因此,它们的组合式是动态决定的,这就是在 Java 中动态双重分派的实现机制。

总结

  在现实情况下,我们要根据具体的情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,使用访问者模式是否能够优化我们的代码,而不是使我们的代码变得更复杂。在使用一个模式之前,我们应该明确它的使用场景、它能解决什么问题等,以此来避免滥用设计模式的现象,所以,在学习设计模式时,一定要理解模式的适用性以及优缺点。
  访问者模式的优点:

  • 各角色职责分离,符合单一职责原则
  • 能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能,具有良好的扩展性;
  • 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化;
  • 灵活性。
  访问者模式的缺点:
  • 具体元素对访问者公布细节,违反了迪米特原则
  • 具体元素变更时导致修改成本大;
  • 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。

源码下载

  https://github.com/zhaozepeng/Design-Patterns/tree/master/VisitorPattern

引用

https://en.wikipedia.org/wiki/Visitor_pattern
http://www.cnblogs.com/java-my-life/archive/2012/06/14/2545381.html
http://blog.csdn.net/jason0539/article/details/45146271
https://my.oschina.net/sel/blog/215959

以上是关于java/android 设计模式学习笔记(24)---访问者模式的主要内容,如果未能解决你的问题,请参考以下文章

java/android 设计模式学习笔记---工厂方法模式

java/android 设计模式学习笔记---对象池模式

java/android 设计模式学习笔记---观察者模式

java/android 设计模式学习笔记目录

java/android 设计模式学习笔记目录

java/android 设计模式学习笔记(12)---组合模式