Java模拟 双分派Double Dispatch

[最后编辑2015.10.23]

本节应用命令模式,在Java中模拟双分派。理解本节后,访问者模式(visitor pattern)手到擒来。

1. 分派

分派/ dispatch是指运行环境按照对象的实际类型为其绑定对应方法体的过程。【注意:本文的分派,指运行时的绑定。不要讨论什么静态多分派、动态多分派】

例如有抽象类X及其两个子类X1、X2,X定义了抽象方法m(),子类X1、X2给出自己的实现。

        X obj =(X)God.create("3-18-X");

        obj.m();//配置文件取X1或X2时,动态绑定

这里的消息表达式obj.m(),只涉及“一个对象”的实际类型,当然该对象比较特殊,每一个消息表达式obj.m(a,b)只有 一个消息接收者按照消息接收者的实际类型,绑定该实际类型提供的方法体,称为单分派(single dispatch)。

那么,对于消息表达式a.m(b,c),如果能够按照a、b和c的实际类型为其绑定对应方法体,则称为三分派。简单起见,研究双分派(double dispatch)。【相关概念,可以参考《设计模式.5.11访问者模式》p223】

例如有foo(X)、foo(X1)和foo(X2)这些重载的方法(放在OverloadUnit类中),显然,对于测试代码

        X obj =(X)God.create("3-18-X");

        new OverloadUnit().foo(obj);//配置文件取X1或X2时,静态绑定

Java在编译时,就为foo(obj)按照obj的声明类型,静态地绑定了方法体。(Java重载方法的匹配算法,请参考[编程导论·2.3.1])。Java、C#等语言不支持双分派。.

忽略动态绑定的消息接收者部分,使用Java模拟双分派需要解决的问题仅仅是区分重载的方法。一个可用的方案,是使用运行时类型识别(Run-Time TypeIdentification、RTTI)技术,即用关键字instanceof判断实际类型,foo_RTTI(X )可以正确地调用(或取代)上面重载的方法的3个方法。

package method.command.doubleDispatch.xPackage;
import static tool.Print.*;
public class OverloadUnit{
    //public void foo(X x) { pln("foo(X)"); }
    public void foo(X1 x){ pln("foo(X1)");}
    public void foo(X2 x){ pln("foo(X2)");}
    public void foo_RTTI(X x){
        if(x instanceof X1){
            X1 temp = (X1)x;
            foo(temp);
        }else if(x instanceof X2){
            foo((X2)x);
        }else{
            foo(x);
        }
    }  
}
//Test类
    public static void testRTTI(){
        X objParam = (X)God.create("3-18-Param");
        pln("X obj = new "+objParam.getClass().getSimpleName() + "();");
        p("foo(X obj) -> ");
        new OverloadUnit().foo_RTTI(objParam);
    }

从配置文件取X、X1或X2时,测试达到了预期:输出为

X obj = new X2();

foo(X obj) -> foo(X2)

X obj = new X1();

foo(X obj) -> foo(X1)

虽然foo_RTTI(X)参数的声明类型为父类X,其代码按照实际类型将参数向下造型后,再调用适当的重载方法。RTTI代码简洁,但①使用分支语句不够优雅;②RTTI将占用较多的运行时间和空间。此外,要注意具体类型判断在前。

2. Foo命令

在Java中模拟双分派,将不采用RTTI而是使用命令模式。命令模式使得调用者Test无视OverloadUnit的被调的方法名(即使同名)。

Test发出foo (X)消息时,希望能够按照参数X的实际类型执行OverloadUnit中的适当代码。因此,执行者已知为OverloadUnit,将它定义了命令Foo的静态域。

具体命令将和foo_RTTI(X)做相同的事情——按照实际类型将参数向下造型。【原来的版本中Foo的handleFoo(Z z),分成了两个抽象方法,handleFoo()类似熟悉的exe(),而setZ(Z z)是一个注入接口。】

package method.command.doubleDispatch.xPackage;
public interface Foo{
    static OverloadUnit receiver = new OverloadUnit();//执行者已知
    public  void handleFoo(X x);
}
class FooX1 implements Foo{    
    @Override  public void handleFoo(X x){
        receiver.foo((X1)x);
    }  
}
//Test类
    public static void testFoo(){
        X objParam = (X)God.create("3-18-Param");//X2
        Foo cmd =(Foo)God.create("3-18-Foo");//?创建具体命令的对象,应该由objParam完成。
        cmd.handleFoo(objParam);
    }

这个版本有一个明显的问题:调用者Test应该创建参数X的对象objParam,但是它不应该创建一个具体命令。一旦参数为X1的对象,就意味着具体命令必须为FooX1。调用者Test分别创建参数对象和具体命令,无法保证两者匹配。换言之,X类层次负责创建对应的具体命令的对象。即X类层次应用工厂方法模式,作为Foo类层次的工厂。

package method.command.doubleDispatch.xPackage;
public abstract class X{ 
    public abstract void m();
    public abstract Foo makeFoo();
}
//Test类
    public static void testFoo(){
        X objParam = (X)God.create("3-18-Param");//X2
        Foo cmd = objParam.makeFoo();
        cmd.handleFoo(objParam);
    }

3. 不想解耦

命令模式使得调用者Test与OverloadUnit解耦。但是,Test希望发出new OverloadUnit().foo (X)这样的命令!它不希望与OverloadUnit解耦。

假定X是抽象类,OverloadUnit中定义重载foo(X x)作为Test调用的接口(门面模式)。对于用户而言,OverloadUnit只有一个接口foo(Z),所以OverloadUnit内部可以提供不同名的方法,也可以使用重载的方法。[编程导论·2.3.1] 中说明:“重载一个方法,真正做的事情是定义了若干不同的方法,不过‘碰巧’使用了相同的方法名”。对于调用者而言它们是透明的。

[GoF]的访问者模式的类图中,采用的是fooX1(X1 x)、fooX2(X2 x)形式

public class OverloadUnit{
    public void foo(X x) { 
        Foo cmd = x.makeFoo();//多态
        cmd.handleFoo(x);
    }

    /*public*/ void foo(X1 x){ pln("foo(X1)");}
    /*public*/ void foo(X2 x){ pln("foo(X2)");}
}

//Test类
    public static void testOverload(){
        X objParam = (X)God.create("3-18-Param");//X1
        X objParam2 = (X)God.create("3-18-Param2");//X2
        new OverloadUnit().foo(objParam);
        new OverloadUnit().foo(objParam2);
    }
4. 改进

1)OverloadUnit的foo(X x)中,引入了Foo。按照LoD,OverloadUnit不需要了解Foo。换言之,OverloadUnit应该仅仅调用X的某个方法如handleFoo(),该方法完成现在OverloadUnit.foo(X x)的功能。

2) 合并X和具体命令FooX。为了保留X系列的代码,重新定义Z系列。

package method.command.doubleDispatch;
/**
 * 合并method.command.doubleDispatch.xPackage包中的X和具体命令FooX
 */
public abstract class Z  {
    static OverloadUnit receiver = new OverloadUnit();//执行者已知
    public abstract void m();
//     public abstract Foo makeFoo();//工厂方法,将Foo并入Z后,注释掉
//     public abstract void handleFoo(Z z);//被handleFoo()合并
    public abstract void handleFoo();
}
package method.command.doubleDispatch;
/**
 * 移植FooZ1中的代码后,删除了FooZ1
 */
public class Z1 extends Z {
    @Override public void m(){   System.out.println(" Z1.m()");    }
    //命令
    @Override public void handleFoo(){//移植FooX1中的代码
        receiver.foo(this);
//         this.handleFoo(this);
    }
//     @Override  public void handleFoo(Z x){//移植X中的代码,合并FooX1后,并入handleFoo()
//         receiver.foo((Z1)x);
//     }
}
//Test类
    public static void testOverload(){
        Z obj = (Z)God.create("3-18-Z1");//Z1对象
        new OverloadUnit().foo (obj);
        System.out.println();
        obj = (Z)God.create("3-18-Z2");//Z2对象         
        new OverloadUnit().foo (obj);
    }
3)命令的执行者OverloadUnit

Test不希望与OverloadUnit解耦,那么Z就不应该再拥有静态成员变量OverloadUnit,不应该一次执行中创建两次OverloadUnit对象。Z可以采用接口注入的方式注入OverloadUnit对象,或者将它作为handleFoo()的参数。

package method.command.doubleDispatch;
/**
 * 合并method.command.doubleDispatch.xPackage包中的X和具体命令FooX
 */
public abstract class Z  {
    public abstract void m();
    public abstract void handleFoo(OverloadUnit receiver);//命令
}


网上看见一个段子:

【双重分派:

对了,你在上面的例子中体会到双重分派的实现了没有?

首先在客户程序中将具体访问者模式作为参数传递给具体元素角色(加亮的地方所示)。这便完成了一次分派

进入具体元素角色后,具体元素角色调用作为参数的具体访问者模式中的visitor方法,同时将自己(this)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行(加亮的地方所示)。这便完成了第二次分派。】


Java模拟 双分派Double Dispatch_第1张图片

你可能感兴趣的:(java,设计模式,命令,访问者)