[最后编辑2015.10.23]
本节应用命令模式,在Java中模拟双分派。理解本节后,访问者模式(visitor pattern)手到擒来。
分派/ 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将占用较多的运行时间和空间。此外,要注意具体类型判断在前。在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); }
命令模式使得调用者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)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行(加亮的地方所示)。这便完成了第二次分派。】