Featured image of post 分派与访问者模式

分派与访问者模式

java 中关于重载和重写的重新定义,访问者模式基于分派实现

最近在看访问者模式的时候,看到了“分派”这个词,脑子里一时没有想起来在哪见过,查阅相关资料之后发现《深入理解Java虚拟机》这本书中有专门提过这个概念,遂去重新阅读相关章节内容,虽然能够理解其中含义,却总感觉这里面有点怪怪的地方,似懂非懂的感觉一直在脑门上方围绕,故作此文,以做总结。

高大上的词汇

在《深入理解Java虚拟机(第二版)》的第八章中有关于宗量分派单分派多分派静态分派动态分派这些词汇的详细介绍,我在这里随便摘抄两个,看看你能否理解是什么意思:

方法的接收者与方法的参数统称为方法的宗量

根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

是否有点不大明白?

这里先来说一下什么叫做方法的接收者:一个方法所属的对象叫做方法的接收者。例如对象a中有一个方法b,那么方法b的接收者就是对象a

然后什么叫统称,统称:总的叫起来,总的名称,也就是说方法的接收者叫做宗量,方法的参数也叫做宗量。

接下来,我按照我理解的意思,来分别介绍一下静态分派动态分派以及单分派多分派

静态分派

我们在学习java或者其他语言的时候应该都有了解方法重载方法重写的定义,在java中,设计方法重载的语法规则的时候,并不是在运行时根据传递进方法的参数的实际类型,来决定调用哪个重载函数,而是在编译时,根据传递进方法的参数的声明类型(也就是编译时类型),来决定调用哪个重载函数。

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface Animal {
    
}

public class Dog implements Animal {
  
}

public class Cat implements Animal {
  
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class DemoMain {
    public static void main(String[] args) {
        DemoMain demoMain = new DemoMain();
        Animal dog = new Dog();
        Animal cat = new Cat();
        demoMain.sleep(cat);
        demoMain.sleep(dog);
    }
    
    private void sleep(Dog dog) {
        System.out.println("dog sleep");
    }
    
    private void sleep(Cat cat) {
        System.out.println("cat sleep");
    }
    
    private void sleep(Animal animal) {
        System.out.println("animal sleep");
    }
}

我们在使用上面的方式创建对象的时候,左边的Animal就是声明类型(书中叫做静态类型),右边的Dog或者Cat叫做实际类型,也就是说在方法重载时,只会依据声明类型:Animal作为参数来确定调用的方法,所以很明显这里的运行结果是:

1
2
animal sleep
animal sleep

所以,我们能够给出如下结论:

所有以来静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

动态分派

有了静态分派作为背景,那么了解动态分派就相对较为简单了。

在静态分派中我有提到过,编译期确定,所以对应的,动态分派是在运行时才能确定执行的是哪个对象的哪个方法。这对应的其实就是方法的重写。

稍微将上面的例子改一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface Animal {
    void sleep();
}

public class Cat implements Animal{
    @Override
    public void sleep() {
        System.out.println("cat sleep");
    }
}

public class Dog implements Animal {
    @Override
    public void sleep() {
        System.out.println("dog sleep");
    }
}

public class DemoMain {
    public static void main(String[] args) {

        Animal dog = new Dog();

        Animal cat = new Cat();

        dog.sleep();
        cat.sleep();
    }
}

先说结果吧:

1
2
dog sleep
cat sleep

这里肯定不能再依据声明时类型来作为真实对象了,因为以Animal作为真实对象,那这里根本走不下去,最终执行的是接口下对应的方法,根本不可能。

从字节码层面来说,其实就是jvm在程序运行时依据栈中的元素指向的实际类型,来执行对应的方法。

单分派&多分派

在本大节开头就已经介绍了单分派多分派,这两个区别其实就是宗量的多少来定的,一个宗量的叫单分派,多于一个宗量的叫多分派。

Java中的分派

java中,静态分派是多分派,动态分派是单分派。

  • 静态分派中,编译器要判断方法接收者的声明类型,确定是哪个类上的方法,这个时候已经判定了接收者这个宗量了;
  • 除此之外,编译器还需要确定参数的静态类型,以确定重载方法中的版本,这个时候又判定了参数这个宗量。

所以静态分派是多分派。

  • 而到了运行期,虚拟机首先就是判断方法接收者的实际类型,去实际类型对应的对象上搜索匹配的方法,这里只判定了接收者的实际类型,方法参数的类型已经不重要了(因为如果存在多个方法名相同,而参数类型不同的方法,会在编译阶段就确定参数类型)。

所以动态分派是单分派。

那么,问题来了,java中能否实现动态多分派呢?继续往下看

模拟实现动态多分派

依据上一节的定义,动态多分派指的是方法的接收者和方法参数类型都是在程序运行时才唯一确定,例如下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ParentClass {
    
}

public class ChildClass extends ParentClass {

}

public interface Animal {
    void sleep(ParentClass parentClass);

    void sleep(ChildClass childClass);
}

public class Cat implements Animal{

    @Override
    public void sleep(ParentClass parentClass) {
        System.out.println("parent cat sleep");
    }

    @Override
    public void sleep(ChildClass childClass) {
        System.out.println("child cat sleep");
    }
}

public class Dog implements Animal {

    @Override
    public void sleep(ParentClass parentClass) {
        System.out.println("parent dog sleep");
    }

    @Override
    public void sleep(ChildClass childClass) {
        System.out.println("child dog sleep");
    }
}

public class DemoMain {
    public static void main(String[] args) {
        ParentClass parentClass = new ChildClass();

        Animal dog = new Dog();

        Animal cat = new Cat();

        dog.sleep(parentClass);
        cat.sleep(parentClass);
    }
}

如果从动态分派角度来说,这里的运行结果应该是:

1
2
child dog sleep
child cat sleep

但真实运行结果却是:

1
2
parent dog sleep
parent cat sleep

相信大家也清楚这其中的原因,无非就是Animal接口内部的方法之间还是重载关系,所以方法参数会在编译期间确定参数类型。

那么能不能模拟实现动态多分派呢?也就是说,不修改main()方法中的任何代码,实现输出child dog sleepchild cat sleep

当然是能的!看下面的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class ParentClass {
    String sleep() {
        return "parent";
    }
}

public class ChildClass extends ParentClass {
    @Override
    String sleep() {
        return "child";
    }
}

public interface Animal {
    void sleep(ParentClass parentClass);

    void sleep(ChildClass childClass);

}

public class Cat implements Animal{

    @Override
    public void sleep(ParentClass parentClass) {
        System.out.println(parentClass.sleep() + " cat sleep");
    }

    @Override
    public void sleep(ChildClass childClass) {
        System.out.println(childClass.sleep() + " cat sleep");
    }

}

public class Dog implements Animal {

    @Override
    public void sleep(ParentClass parentClass) {
        System.out.println(parentClass.sleep() + " dog sleep");
    }

    @Override
    public void sleep(ChildClass childClass) {
        System.out.println(childClass.sleep() + " dog sleep");
    }
}

public class DemoMain {
    public static void main(String[] args) {
        ParentClass parentClass = new ChildClass();

        Animal dog = new Dog();

        Animal cat = new Cat();

        dog.sleep(parentClass);
        cat.sleep(parentClass);
    }

}

运行结果:

1
2
child dog sleep
child cat sleep

通过上面的代码就可以模拟实现动态多分派,究其原理,其实就是在Animal子类的内部进行了一次反转,让原来通过静态分派确定下来的编译时类型,在Animal子类内部又因为调用了对象的方法而发生了运行时的类型确定,也就是动态分派,所以上面的代码发生了两次动态分派,而在外部看来,放佛是实现了动态多分派。

为例让大家看得更清晰,我把main()方法中的代码稍微调整了一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class DemoMain {
    public static void main(String[] args) {
        List<ParentClass> pList = new ArrayList<>();
        ParentClass childClass = new ChildClass();
        ParentClass parentClass = new ParentClass();
        pList.add(parentClass);
        pList.add(childClass);
        Animal dog = new Dog();
        Animal cat = new Cat();
        pList.forEach(clazz -> {
            dog.sleep(clazz);
            cat.sleep(clazz);
        });  
    }
}

此时的运行结果如下:

1
2
3
4
parent dog sleep
parent cat sleep
child dog sleep
child cat sleep

看到没,一个集合内存在一对父子类,而运行的结果和父子类直接相关,并不是一直和父类相关。

而这,也就引出了访问者模式~~

访问者模式

GoF 的《设计模式》一书中,它是这么定义的:

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

翻译过来就是:允许一个或者多个操作在运行时应用到一组对象上,从对象本身中解耦操作。

原文的意思很明确了,就是将对象的操作从对象中单独独立出来,然后通过运行时来确定操作,大意就是对象中不进行任何操作相关的事情,所有操作相关东西都独立出去,然后利用组合的方式在运行时和对象“关联”起来,至于怎么“关联”,我们还是来看代码:

  • 未修改前
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public interface ObjectFather {
    void operationA();

    void operationB();
}

public class ObjectSonA implements ObjectFather {
    @Override
    public void operationA() {
        System.out.println("sonA: operationA");
    }

    @Override
    public void operationB() {
        System.out.println("sonA: operationB");
    }
}

public class ObjectSonB implements ObjectFather {
    @Override
    public void operationA() {
        System.out.println("sonB: operationA");
    }

    @Override
    public void operationB() {
        System.out.println("sonB: operationB");
    }
}

public class Demo {
    public static void main(String[] args) {
        ObjectFather c1 = new ObjectSonA();
        ObjectFather c2 = new ObjectSonB();
        List<ObjectFather> list = new ArrayList<>();
        list.add(c2);
        list.add(c1);
        for (ObjectFather clazz : list) {
            clazz.operationA();
        }

        for (ObjectFather clazz : list) {
            clazz.operationB();
        }
    }
}

从上面的代码中我们可以看到所有针对对象中的操作,都是存在于对象内部的,没有和对象分离,这样如果此时需要新增一种操作operationC,那么上面的代码几乎每一个类都需要改动,有点“牵一发而动全身”的味道了。

  • 修改后
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//对象
public interface ObjectFather {
    void operation(Operation operation);
}

public class ObjectSonA implements ObjectFather {
    @Override
    public void operation(Operation operation) {//accept
        operation.doSomeOperation(this, "sonA:");
    }
}

public class ObjectSonB implements ObjectFather {

    @Override
    public void operation(Operation operation) {//accept
        operation.doSomeOperation(this, "sonB:");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//操作
public interface Operation {
    void doSomeOperation(ObjectFather objectFather, String message);
}

public class OperationA implements Operation {
    @Override
    public void doSomeOperation(ObjectFather objectFather, String message) {
        //do something
        System.out.println(message + " operationA");
    }
}

public class OperationB implements Operation {
    @Override
    public void doSomeOperation(ObjectFather objectFather, String message) {
        //do something
        System.out.println(message + " OperationB");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
    public static void main(String[] args) {
        ObjectFather c1 = new ObjectSonA();
        ObjectFather c2 = new ObjectSonB();

        Operation operationA = new OperationA();
        Operation operationB = new OperationB();

        List<ObjectFather> list = new ArrayList<>();
        list.add(c2);
        list.add(c1);

        for (ObjectFather objectFather : list) {
            objectFather.operation(operationA);
        }

        for (ObjectFather objectFather : list) {
            objectFather.operation(operationB);
        }

    }

可以看到,在修改后的代码中,我们将对象和操作完全分离,操作的添加和修改完全和对象没有关系,对象做的唯一一件事情就是接收操作,然后执行操作,它不关心是何种操作,对象只管过来就行;反观操作,每一个类中的操作都是唯一的,如果需要新添加操作,只需要新增一个类即可,不会影响到现有代码。

从访问者模式中可以看到,它深度利用了java中的多态和分派概念,将复杂的逻辑解耦成彼此不会有太多影响的独立结构体,从而提升了整个结构的稳定性。

总结

本文总体上就介绍了两个东西,一个是分派,一个是访问者模式,分派作为访问者模式实现的基础,让java中的重载和重写又有了新的一层理解;而访问者模式的成功运用将会极大解耦代码的关联度,虽然它比较难以理解,但是相信如果能够成功运用将会给你带来大大的便利性(尤其是遇到需求一直变动,你的代码不稳定会导致你一直在更改你的所有代码)。

老规矩,文章最后还是又一个问题需要思考:

访问者模式固然不错,但是是否存在替代的可能?我这里自问自答一下!

存在替代的方式,例如使用策略模式加工厂模式来替代,当然如果操作之间存在着关联性,比如操作B可以做的基础是操作A已经完成,那么还可以考虑使用责任链模式。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus