• 《Java设计模式》之訪问者模式


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

    分派的概念

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

    1. List list = null;  
    2. list = new ArrayList();  


     

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

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

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

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

     静态分派

      Java通过方法重载支持静态分派。

    用墨子骑马的故事作为样例。墨子能够骑白马或者黑马。

    墨子与白马、黑马和马的类图例如以下所看到的:

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

    1. package com.bankht.Visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:41:24 
    6.  *  
    7.  * @类说明 :在这个系统中。墨子由Mozi类代表 
    8.  */  
    9. public class Mozi {  
    10.   
    11.     public void ride(Horse h) {  
    12.         System.out.println("骑马");  
    13.     }  
    14.   
    15.     public void ride(WhiteHorse wh) {  
    16.         System.out.println("骑白马");  
    17.     }  
    18.   
    19.     public void ride(BlackHorse bh) {  
    20.         System.out.println("骑黑马");  
    21.     }  
    22.   
    23.     public static void main(String[] args) {  
    24.         Horse wh = new WhiteHorse();  
    25.         Horse bh = new BlackHorse();  
    26.         Mozi mozi = new Mozi();  
    27.         mozi.ride(wh);  
    28.         mozi.ride(bh);  
    29.     }  
    30.   
    31. }  


     

      显然,Mozi类的ride()方法是由三个方法重载而成的。

    这三个方法分别接受马(Horse)、白马(WhiteHorse)、黑马(BlackHorse)等类型的參数。

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

      为什么呢?两次对ride()方法的调用传入的是不同的參数。也就是wh和bh。

    它们尽管具有不同的真实类型。可是它们的静态类型都是一样的,均是Horse类型。

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

     动态分派

      Java通过方法的重写支持动态分派。

    用马吃草的故事作为样例。代码例如以下所看到的:

    1. package com.bankht.Visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:41:46 
    6.  *  
    7.  * @类说明 :Java通过方法的重写支持动态分派。

      用马吃草的故事作为样例 

    8.  */  
    9. public class Horse {  
    10.   
    11.     public void eat() {  
    12.         System.out.println("马吃草");  
    13.     }  
    14.   
    15. }  


     

    1. package com.bankht.Visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:43:15 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class WhiteHorse extends Horse {  
    10.   
    11.     @Override  
    12.     public void eat() {  
    13.         System.out.println("白马吃草");  
    14.     }  
    15. }  


     

    1. package com.bankht.Visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:42:08 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class BlackHorse extends Horse {  
    10.   
    11.     @Override  
    12.     public void eat() {  
    13.         System.out.println("黑马吃草");  
    14.     }  
    15. }  


     

    1. package com.bankht.Visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:42:34 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class Client {  
    10.   
    11.     public static void main(String[] args) {  
    12.         Horse h = new BlackHorse();  
    13.         h.eat();  
    14.     }  
    15.   
    16. }  


    执行一下:

    1. 黑马吃草  

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

      所以,问题的核心就是Java编译器在编译时期并不总是知道哪些代码会被运行,由于编译器只知道对象的静态类型,而不知道对象的真实类型。而方法的调用则是依据对象的真实类型,而不是静态类型。

    这样一来。上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,打印的是“黑马吃草”。

     分派的类型

      一个方法所属的对象叫做方法的接收者,方法的接收者与方法的參数统称做方法的宗量。比方以下样例中的Test类

    1. public class Test {  
    2.   
    3.     public void print(String str){  
    4.         System.out.println(str);  
    5.     }  
    6. }  


     

      在上面的类中,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类

    1. public abstract class West {  
    2.       
    3.     public abstract void goWest1(SubEast1 east);  
    4.       
    5.     public abstract void goWest2(SubEast2 east);  
    6. }  


     

      SubWest1类

    1. public class SubWest1 extends West{  
    2.       
    3.     @Override  
    4.     public void goWest1(SubEast1 east) {  
    5.           
    6.         System.out.println("SubWest1 + " + east.myName1());  
    7.     }  
    8.       
    9.     @Override  
    10.     public void goWest2(SubEast2 east) {  
    11.           
    12.         System.out.println("SubWest1 + " + east.myName2());  
    13.     }  
    14. }  


     SubWest2类

    1. public class SubWest2 extends West{  
    2.     @Override  
    3.     public void goWest1(SubEast1 east) {  
    4.           
    5.         System.out.println("SubWest2 + " + east.myName1());  
    6.     }  
    7.       
    8.     @Override  
    9.     public void goWest2(SubEast2 east) {  
    10.           
    11.         System.out.println("SubWest2 + " + east.myName2());  
    12.     }  
    13. }  


     East类

    1. public abstract class East {  
    2.   
    3.     public abstract void goEast(West west);  
    4. }  


     

      SubEast1类

    1. public class SubEast1 extends East{  
    2.     @Override  
    3.     public void goEast(West west) {  
    4.         west.goWest1(this);  
    5.     }  
    6.       
    7.     public String myName1(){  
    8.         return "SubEast1";  
    9.     }  
    10. }  


     

      SubEast2类

    1. public class SubEast2 extends East{  
    2.     @Override  
    3.     public void goEast(West west) {  
    4.         west.goWest2(this);  
    5.     }  
    6.       
    7.     public String myName2(){  
    8.         return "SubEast2";  
    9.     }  
    10. }  


     

      client类

    1. public class Client {  
    2.   
    3.     public static void main(String[] args) {  
    4.         //组合1  
    5.         East east = new SubEast1();  
    6.         West west = new SubWest1();  
    7.         east.goEast(west);  
    8.         //组合2  
    9.         east = new SubEast1();  
    10.         west = new SubWest2();  
    11.         east.goEast(west);  
    12.     }  
    13.   
    14. }  


    执行结果例如以下

    --------------------------------------------------------------------------------  
    SubWest1 + SubEast1
    SubWest2 + SubEast1
    --------------------------------------------------------------------------------


     

      系统执行时。会首先创建SubWest1和SubEast1对象,然后client调用SubEast1的goEast()方法,并将SubWest1对象传入。因为SubEast1对象重写了其超类East的goEast()方法。因此。这个时候就发生了一次动态的单分派。当SubEast1对象接到调用时,会从參数中得到SubWest1对象。所以它就马上调用这个对象的goWest1()方法,并将自己传入。因为SubEast1对象有权选择调用哪一个对象。因此,在此时又进行一次动态的方法分派。

      这个时候SubWest1对象就得到了SubEast1对象。通过调用这个对象myName1()方法,就能够打印出自己的名字和SubEast对象的名字,其时序图例如以下所看到的:

      因为这两个名字一个来自East等级结构,还有一个来自West等级结构中,因此。它们的组合式是动态决定的。

    这就是动态双重分派的实现机制。

    訪问者模式的结构

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

    数据结构的每个节点都能够接受一个訪问者的调用。此节点向訪问者对象传入节点对象,而訪问者对象则反过来运行节点对象的操作。这种过程叫做“双重分派”。

    节点调用訪问者,将它自己传入,訪问者则将某算法针对此节点运行。

    訪问者模式的示意性类图例如以下所看到的:

      訪问者模式涉及到的角色例如以下:

      ●  抽象訪问者(Visitor)角色:声明了一个或者多个方法操作。形成全部的详细訪问者角色必须实现的接口。

      ●  详细訪问者(ConcreteVisitor)角色:实现抽象訪问者所声明的接口,也就是抽象訪问者所声明的各个訪问操作。

      ●  抽象节点(Node)角色:声明一个接受操作。接受一个訪问者对象作为一个參数。

      ●  详细节点(ConcreteNode)角色:实现了抽象节点所规定的接受操作。

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

      源码

      能够看到,抽象訪问者角色为每个详细节点都准备了一个訪问操作。因为有两个节点,因此,相应就有两个訪问操作。

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:50:06 
    6.  *  
    7.  * @类说明 :能够看到,抽象訪问者角色为每个详细节点都准备了一个訪问操作。因为有两个节点,因此,相应就有两个訪问操作。

       

    8.  */  
    9. public interface Visitor {  
    10.     /** 
    11.      * 相应于NodeA的訪问操作 
    12.      */  
    13.     public void visit(NodeA node);  
    14.   
    15.     /** 
    16.      * 相应于NodeB的訪问操作 
    17.      */  
    18.     public void visit(NodeB node);  
    19. }  


     

      详细訪问者VisitorA类

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:58:04 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class VisitorA implements Visitor {  
    10.     /** 
    11.      * 相应于NodeA的訪问操作 
    12.      */  
    13.     @Override  
    14.     public void visit(NodeA node) {  
    15.         System.out.println("VisitorA:" + node.operationA());  
    16.     }  
    17.   
    18.     /** 
    19.      * 相应于NodeB的訪问操作 
    20.      */  
    21.     @Override  
    22.     public void visit(NodeB node) {  
    23.         System.out.println("VisitorA:" + node.operationB());  
    24.     }  
    25.   
    26. }  


     

      详细訪问者VisitorB类

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:58:21 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class VisitorB implements Visitor {  
    10.     /** 
    11.      * 相应于NodeA的訪问操作 
    12.      */  
    13.     @Override  
    14.     public void visit(NodeA node) {  
    15.         System.out.println("VisitorB:" + node.operationA());  
    16.     }  
    17.   
    18.     /** 
    19.      * 相应于NodeB的訪问操作 
    20.      */  
    21.     @Override  
    22.     public void visit(NodeB node) {  
    23.         System.out.println("VisitorB:" + node.operationB());  
    24.     }  
    25.   
    26. }  


     

      抽象节点类

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:58:57 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public abstract class Node {  
    10.     /** 
    11.      * 接受操作 
    12.      */  
    13.     public abstract void accept(Visitor visitor);  
    14. }  


     详细节点类NodeA

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:59:45 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class NodeA extends Node {  
    10.     /** 
    11.      * 接受操作 
    12.      */  
    13.     @Override  
    14.     public void accept(Visitor visitor) {  
    15.         visitor.visit(this);  
    16.     }  
    17.   
    18.     /** 
    19.      * NodeA特有的方法 
    20.      */  
    21.     public String operationA() {  
    22.         return "NodeA";  
    23.     }  
    24.   
    25. }  


     

      详细节点类NodeB

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午10:59:59 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class NodeB extends Node {  
    10.     /** 
    11.      * 接受方法 
    12.      */  
    13.     @Override  
    14.     public void accept(Visitor visitor) {  
    15.         visitor.visit(this);  
    16.     }  
    17.   
    18.     /** 
    19.      * NodeB特有的方法 
    20.      */  
    21.     public String operationB() {  
    22.         return "NodeB";  
    23.     }  
    24. }  


     

      结构对象角色类。这个结构对象角色持有一个聚集,并向外界提供add()方法作为对聚集的管理操作。通过调用这种方法,能够动态地添加一个新的节点。

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. import java.util.ArrayList;  
    4. import java.util.List;  
    5.   
    6. /** 
    7.  * @author: 特种兵—AK47 
    8.  * @创建时间:2012-7-3 上午11:00:15 
    9.  *  
    10.  * @类说明 : 
    11.  */  
    12. public class ObjectStructure {  
    13.   
    14.     private List<Node> nodes = new ArrayList<Node>();  
    15.   
    16.     /** 
    17.      * 运行方法操作 
    18.      */  
    19.     public void action(Visitor visitor) {  
    20.   
    21.         for (Node node : nodes) {  
    22.             node.accept(visitor);  
    23.         }  
    24.   
    25.     }  
    26.   
    27.     /** 
    28.      * 加入一个新元素 
    29.      */  
    30.     public void add(Node node) {  
    31.         nodes.add(node);  
    32.     }  
    33. }  


     

      client类

    1. package com.bankht.Visitor.visitor;  
    2.   
    3. /** 
    4.  * @author: 特种兵—AK47 
    5.  * @创建时间:2012-7-3 上午11:00:29 
    6.  *  
    7.  * @类说明 : 
    8.  */  
    9. public class Client {  
    10.   
    11.     public static void main(String[] args) {  
    12.         // 创建一个结构对象  
    13.         ObjectStructure os = new ObjectStructure();  
    14.         // 给结构添加一个节点  
    15.         os.add(new NodeA());  
    16.         // 给结构添加一个节点  
    17.         os.add(new NodeB());  
    18.         // 创建一个訪问者  
    19.         Visitor visitor = new VisitorA();  
    20.         os.action(visitor);  
    21.     }  
    22.   
    23. }  


     

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

      准备过程时序图

      首先,这个示意性的client创建了一个结构对象。然后将一个新的NodeA对象和一个新的NodeB对象传入。

      其次。client创建了一个VisitorA对象,并将此对象传给结构对象。

      然后,client调用结构对象聚集管理方法,将NodeA和NodeB节点增加到结构对象中去。

      最后,client调用结构对象的行动方法action(),启动訪问过程。

      訪问过程时序图

      

      结构对象会遍历它自己所保存的聚集中的全部节点,在本系统中就是节点NodeA和NodeB。

    首先NodeA会被訪问到,这个訪问是由下面的操作组成的:

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

      (2)NodeA对象反过来调用VisitorA对象的訪问方法。并将NodeA对象本身传入。

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

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

    訪问者模式的长处

      ●  好的扩展性

      可以在不改动对象结构中的元素的情况下。为对象结构中的元素加入新的功能。

      ●  好的复用性

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

      ●  分离无关行为

      能够通过訪问者来分离无关的行为,把相关的行为封装在一起。构成一个訪问者,这样每个訪问者的功能都比較单一。

    訪问者模式的缺点

      ●  对象结构变化非常困难

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

      ●  破坏封装

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

      

    本文借鉴:http://blog.csdn.net/m13666368773/article/details/7711326

  • 相关阅读:
    把Linq查询返回的var类型的数据 转换为DataTable EF连接查询
    无法更新 EntitySet 因为它有一个 DefiningQuery
    MVC上传文件
    MySql删除表、数据
    LINQ to Entities 不支持 LINQ 表达式节点类型“ArrayIndex”。
    MVC仓储使用join
    MVC仓储执行存储过程报错“未提供该参数”
    Newtonsoft.Json自动升级版本号,导致dll冲突
    MVC中构建Linq条件、排序、Selector字段过滤
    AutoMapper
  • 原文地址:https://www.cnblogs.com/yangykaifa/p/7169509.html
Copyright © 2020-2023  润新知