单分派、双分派及两种设计模式

3,727 阅读7分钟

什么是单分派和双分派

分派(dispatch) 是指按照对象的实际类型为其绑定对应方法体的过程。

例如有X类及其两个子类X1、X2,它们都实现了实例方法m()——通常子类X1、X2的方法前应该加@Override,所以有3个m()。

对于消息表达式a.m(b,c),按照一个对象的实际类型绑定对应方法体,称为单分派。当然,这个“一个对象”比较特殊,每一个消息表达式a.m(b,c)只有一个消息接收者,这个“一个对象”就是指消息接收者,即a.m(b, c)中的a。所以,仅按照消息接收者的实际类型绑定实际类型提供的方法体,即单分派(singledispatch),就是面向对象中的动态绑定!

假设对于消息表达式a.m(b,c),如果能够按照a、b和c的实际类型为其绑定对应方法体,则称为三分派。简单起见,研究双分派(double dispatch)就够了。

所谓的双分派,则是希望a.foo(b)能够 ①按照a的实际类型绑定其override的方法体,而且能够 ②按照b的实际类型绑定其重载的方法即foo(Y)、foo(Y1)、foo(Y2)中的适当方法体。 【相关概念,可以参考《设计模式.5.11访问者模式》p223】

遗憾的是,Java不支持双分派。对于foo(X)、foo(X1)和foo(X2)这些重载的方法,Java在编译时,就为foo(b)按照b的声明类型静态绑定了foo(X)这个的方法体,而不会去判断b的实际类型是X1还是X2。 Java中可以使用运行时类型识别(Run-Time TypeIdentification、RTTI)技术,即使用关键字instanceof判断实际类型。虽然声明类型为父类Y,程序中按照实际类型重新声明temp,并将参数向下造型。RTTI虽然代码简洁,但使用分支语句不够优雅。另外,①程序员还要注意,具体类型判断在前;②RTTI将占用较多的运行时间和空间。

《Java编程思想》中,有句话

Java中除了static方法和final方法(private方法属于final方法)之外,其他所有方法都是后期绑定,也就是运行时绑定,我们不必判断是否应该进行后期绑定-它会自动发生。

这里提到的后期绑定,也只是针对参数的声明类型来选择具体的方法。

依赖设计模式实现双分派

既然Java支持a.m(b)时,按a的具体类型绑定相应的方法,那如果通过某种方式在a.m(b)的实现中,完成了b.m1()的调用,那不就实现“双分派”了吗?纵览GOF23,有两种设计模式完美地支持这一种,分别是命令模式访问者模式

命令模式实现双分派

源码

命令模式的UML图

先说个题外话,相信大家从UML图中可以看出些问题,为什么Client既需要知道Invoker又需要知道Receiver呢,Invoker角色接受Client的命令并执行命令,而真正命令的实施者是Receiver,用《设计模式之禅》里的比方,Invoker是项目经理,Receiver就是干活的码农或美工等等。依照迪米特法则,那Client就不应该知道Receiver。这个问题我们放在后面讨论。下面先看代码:

抽象receiver

public abstract class Receiver {
    abstract void doSth();
}

ConcreteReceiver1

public class ConcreteCommand1 extends Command {
    private Receiver receiver;

    public ConcreteCommand1(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    void execute(Receiver receiver) {
        System.out.println("我是command1, 入参是Receiver");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver1 receiver) {
        System.out.println("我是command1, 入参是ConcreteReceiver1");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver2 receiver) {
        System.out.println("我是command1, 入参是ConcreteReceiver2");
        receiver.doSth();
    }
}

抽象Command

public abstract class Command {
    private Receiver receiver;

    public Command(Receiver receiver) {
        this.receiver = receiver;
    }
    abstract void execute(Receiver receiver);

    abstract void execute(ConcreteReceiver1 receiver);

    abstract void execute(ConcreteReceiver2 receiver);

    public Receiver getReceiver() {
        return receiver;
    }
}

ConcreteCommand1

public class ConcreteCommand1 extends Command {
    public ConcreteCommand1(Receiver receiver) {
        super(receiver);
    }

    @Override
    public Receiver getReceiver() {
        return super.getReceiver();
    }

    @Override
    void execute(Receiver receiver) {
        System.out.println("我是command1, 入参是Receiver");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver1 receiver) {
        System.out.println("我是command1, 入参是ConcreteReceiver1");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver2 receiver) {
        System.out.println("我是command1, 入参是ConcreteReceiver2");
        receiver.doSth();
    }
}

ConcreteCommand2

public class ConcreteCommand2 extends Command {
    public ConcreteCommand2(Receiver receiver) {
        super(receiver);
    }

    @Override
    void execute(Receiver receiver) {
        System.out.println("我是command2, 入参是Receiver");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver1 receiver) {
        System.out.println("我是command2, 入参是ConcreteReceiver1");
        receiver.doSth();
    }

    @Override
    void execute(ConcreteReceiver2 receiver) {
        System.out.println("我是command2, 入参是ConcreteReceiver2");
        receiver.doSth();
    }

    @Override
    public Receiver getReceiver() {
        return super.getReceiver();
    }
}

Invoker

public class Invoker {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void act() {
        this.command.execute(command.getReceiver());
    }
}

client

public class Client {
    public static void main(String[] args) {
        Invoker invoker = new Invoker();

        Receiver receiver1 = new ConcreteReceiver1();
        Receiver receiver2 = new ConcreteReceiver2();

        Command command1 = new ConcreteCommand1(receiver1);
        Command command2 = new ConcreteCommand2(receiver2);

        invoker.setCommand(command1);
        invoker.act();
    }
}

执行结果

我是command1, 入参是Receiver
receiver1 处理命令1

分析

为了凑a.m(b)这种格式,弄得很丑,大家见谅。Client中,Receiver和Command都是按接口声明的,当执行到invoker.setCommand(command1); invoker.act();时,程序走至

a即this.command,Java可以找到具体的实现类;而b的声明类型是Receiver,Java不会去识别,因此可以看作ConcreteCommand1.execute(Receiver);,因此Java绑定了ConcreteCommand1中的第一个方法

我们看到的打印输出就是我是command1, 入参是Receiver

在这个方法中,receiver又成为了a.m(b)中的a,因为Java又可以根据其实际类型进行方法绑定,因此跑到ConcreteReceiver1中,为了不要脸地硬捧a.m(b),这里又罗哩罗嗦地写了三个重载方法。这时b是ConcreteCommand1,因此找到重载方法

看到打印输出receiver1 处理命令1

最佳实践

《设计模式之禅》也提到了我开篇的疑惑,Client为什么要知晓Receiver的存在呢?事实上我们在实际工作中,没有人真的那么干。引用一段书中的原文:

每一个模式到实际应用的时候都有一些变形,命令模式的Receiver在实际应用中一般都会被封装掉(除非非常必要,例如撤销处理),那是因为在项目中:约定的优先级最高,每一个命令是对一个或多个Receiver的封装,我们可以在项目中通过有意义的类名或命令名处理命令角色和接收者角色的耦合关系(这就是约定),减少高层模块(Client类)对低层模块(Receiver角色类)的依赖关系,提高系统整体的稳定性。因此,建议大家在实际的项目开发时采用封闭Receiver的方式(当然了,仁者见仁,智者见智),减少Client对Reciver的依赖。

访问者模式

了解访问者模式的朋友看到这肯定会说,这tm哪里是命令模式啊,明明是披着命令模式皮的访问者模式嘛!确实,为了想方设法说明双分派,已经把命令模式搞变态了,不妨好好看下访问者模式。举《设计模式之禅》的例子

演员演电影角色,一个演员可以扮演多个角色,我们先定义一个影视中的两个角色:功夫主角和白痴配角

public interface Role { 
//演员要扮演的角色 
} 
public class KungFuRole implements Role { 
//武功天下第一的角色 
} 
public class IdiotRole implements Role { 
//一个弱智角色 
}

角色有了,我们再定义一个演员抽象类

public abstract class AbsActor {
    //演员都能够演一个角色 
    public void act(Role role){
        System.out.println("演员可以扮演任何角色");
    }
    //可以演功夫戏 
    public void act(KungFuRole role){
        System.out.println("演员都可以演功夫角色");
    }
}

很简单,这里使用了Java的重载,我们再来看青年演员和老年演员,采用覆写的方式来 细化抽象类的功能

public class YoungActor extends AbsActor {
    //年轻演员最喜欢演功夫戏 
    public void act(KungFuRole role){
        System.out.println("最喜欢演功夫角色");
    }
}
public class OldActor extends AbsActor {
    //不演功夫角色 
    public void act(KungFuRole role){
        System.out.println("年龄大了,不能演功夫角色");
    }
}

覆写和重载都已经实现,我们编写一个场景,

public class Client {
    public static void main(String[] args) {
//定义一个演员 
        AbsActor actor = new OldActor();
//定义一个角色 
        Role role = new KungFuRole();
//开始演戏 
        actor.act(role);
        actor.act(new KungFuRole());
    }
}

得到输出结果

演员可以扮演任何角色
年龄大了,不能演功夫角色 

使用上节提到的介绍的方法,可以非常轻松地分析出双分派的实现原理。

深层原理

大家可以发现,通过设计模式实现的双分派,其实是“伪双分派”,至少深层的原理,需要阅读更多资料,等我读完《深入理解Java虚拟机》后,会回来把这一节补上。

参考文档

www.iteye.com/topic/11307… www.voidcn.com/article/p-d… 《设计模式之禅》 《Java编程思想》