本节应用命令模式,在Java中模拟双分派。理解本节后,访问者模式(visitor
pattern)手到擒来。
1. 单分派
分派/ dispatch是指如何给一个消息绑定其方法体。Java、C#等仅仅支持单分派(singledispatch)而不支持双分派(double
dispatch)。【相关概念,参考《设计模式.5.11访问者模式》p223】
对于消息a.foo(b),假设有父类X及其两个子类X1、X2,a声明为X类型变量;有父类Y及其两个子类Y1、Y2,b声明为Y类型变量,而且X、X1和X2都各自准备了foo(Y)、foo(Y1)和foo(Y2)方法,请问a.foo(b)将执行的方法体是3*3=9个方法中的哪一个?
当前主流的面向对象语言如C++、Java、C#等,仅仅支持单分派(singledispatch)。例程3-18中,目前可以不管a.foo(b)中的参数b,我们仅仅看消息a.m()好了。假定X、X1和X2都各自准备了m(),则a.m()按照a的实际类型绑定其override的方法体,这就是面向对象中的动态绑定。
所谓的双分派,则是希望a.foo(b)能够①按照a的实际类型绑定其override的方法体,而且能够②按照b的实际类型绑定其重载的方法即foo(Y)、foo(Y1)、foo(Y2)中的适当方法体。显然,Java不支持后者——即不支持双分派。Java在编译时,就为foo(b)按照b的声明类型静态绑定了foo(Y)这个的方法体。Java重载方法的匹配算法,请参考【编程导论·2.3.1】。
例程 3 18 单分派 package method.command.doubleDispatch; import static tool.Print.*; public abstract class X{ public void m(){ pln(" X.m()"); } public void foo(X x){ p(" X.foo(X)-"); x.m(); } public void foo(X1 x1){ p(" X.foo(X1)-"); x1.m(); }// foo(X2 x2) 略 }//子类X1、X2的代码,略 package method.command.doubleDispatch; import tool.God; public class Test{ public static void X单分派(){ X a = (X)God.create("3-18-X1"); X b = (X)God.create("3-18-X2"); a.m(); a.foo(b); } }
|
简单起见,消息a.foo(b)的中a,b的类型均为X。双分派(double dispatch)即在选择一个方法体时需要根据消息接收者a和参数b两者的运行时类型(实际类型)进行绑定,Javabu支持。为了说明这一点,上面的代码中,X显得比较怪异的包含了foo(X)、foo(X1)、foo(X2)方法(父类X依赖其子类),而且子类X1、X2的override了重载的foo()方法代码。
Test.X单分派() 的运行结果:
X1.m() // 动态绑定 X1.foo(X)-X2.m() // 动态绑定X1的foo,而静态绑定foo(X). |
那么,b.foo(a);的运行结果:X2.foo(X)-X1.m()
如果对改写的动态绑定和重载的静态绑定,已经清楚了,也即清楚地知道:Java支持单分派而不支持双分派,下面对应地编写Y、Y1和Y2仅保留各自准备的m(),而Y系列中删除各种foo方法,在OverloadFoo类中专注如何模拟双分派。
package method.command.doubleDispatch; import static tool.Print.*; public class Y{ public void m(){ pln(" Y.m()"); } } |
package method.command.doubleDispatch; import static tool.Print.*; public class Y1 extends Y{ @Override public void m(){ pln("Y1.m()"); } } |
package method.command.doubleDispatch; import static tool.Print.*; /** * OverloadFoo.java. * * @author yqj2065 * @version 0.1 */ public class OverloadFoo{ public void foo(Y y) { y.m();pln("foo(Y)"); } public void foo(Y1 y){ y.m();pln("foo(Y1)");} public void foo(Y2 y){ y.m();pln("foo(Y2)");} /** * (Run-Time Type Identification、RTTI */ public void foo_RTTI(Y y){ if(y instanceof Y1){ pln("foo(Y1)"); }else if(y instanceof Y2){ pln("foo(Y2)"); }else{ pln("foo(Y)"); } } } |
Java中可以使用运行时类型识别(Run-Time TypeIdentification、RTTI)技术即使用关键字instanceof判断实际类型。因而,一个权宜之计是删除三个重载的foo()方法,而编写方法foo_RTTI
(Y )。虽然foo_RTTI (Y) 代码简洁,但是,使用分支语句不够优雅。
如果到处使用if-else的话,很多模式就失业了。
2.命令模式区分重载的方法
【编程导论·2.3.1】 中说明:“重载一个方法,真正做的事情是定义了若干不同的方法,不过‘碰巧’使用了相同的方法名”。
调用foo(Y)的模块如Test,在它看来重载foo(X)、foo(X1)、foo(X2)也好,不同名的fooX()、fooX1()、fooX2()也好,Test希望进行统一的调用——无视被调的方法名,我们可以采用命令模式。
package method.command.doubleDispatch; public abstract class Command{ OverloadFoo handler = new OverloadFoo(); public abstract void foo(Y y);//变化:执行者已知OverloadFoo } |
package method.command.doubleDispatch; public class FooY1 extends Command { @Override public void foo(Y y) { handler.foo((Y1)y); } }//FooY和 FooY2略 |
这个Command和3.4 命令模式(5.2)中简单的Command接口有些小小的进步:命令的执行者已知为OverloadFoo(因为它包括了3个重载的foo方法);抽象方法foo带有参数。
Command的子类FooY1,指明执行者调用重载的foo(Y1)方法。
public static void 模拟双分派(){ Y y = (Y)God.create("3-18-Y");//Y1对象 Command cmd = new FooY(); cmd.foo(y); cmd = new FooY1(); cmd.foo(y); cmd = new FooY2();//任务不可执行 //cmd.foo(y); } |
现在,创建一个Y对象(实际类型Y1)后,按照不同的命令,测试结果:
Y1.m() foo(Y) Y1.m() foo(Y1) |
命令模式,使得用户类Test无视被调的方法名,下达统一的命令foo(y);而执行者按照命令对象的不同,执行不同的方法体——这里就将重载的方法区分开来了。
图1 应用命令模式
3.合并类层次
上图中有两个类层次Y和Command,图形显得比较复杂。我们发现Command的普适命令foo(Y y)在其子类FooY1中的代码为:
@Override public void foo(Y y) { handler.foo((Y1)y); } |
我们如何利用Java的多态性避免这种指定性的强制类型转换呢?要点就是命令的执行者不在是固定的
OverloadFoo handler = new OverloadFoo();
而是FooY1自己——命令执行者将是Y1和Y2!
现在,开启Z系列。
Y系列时的 Command对应接口Foo,普适命令foo(Y y)对应为wi参数的handleFoo()。
package method.command.doubleDispatch; public interface Foo{ public void handleFoo(); } |
与X和Y对应的Z,与X不同之处为foo(Foo )!
Z的类层次成为Command/Foo的子类型。Z implements Foo使得Z的子类自动成为Foo的子类型,(其实Z本身不需要成为
Foo的子类型,你可以将Z的所有子类Z1、Z2 implements Foo)
package method.command.doubleDispatch; import static tool.Print.*; public abstract class Z implements Foo{ public void m(){ pln(" Z.m()"); } public void foo(Foo z ){//示例代码,可以为空方法体 p(" Z.foo(Foo)-"); this.m(); } //@Override public void handleFoo(){} //可有可无 } |
现在,Z1的代码如下:
package method.command.doubleDispatch; import static tool.Print.*; public class Z1 extends Z { @Override public void m(){ pln(" Z1.m()"); } /*事实上,意味着重载foo(Z1)*/ @Override public void foo(Foo z ){ p("Z1."); z.handleFoo();//执行者z动态绑定 this.m(); } private void foo(){ p("foo(Z1)-"); } @Override public void handleFoo(){ this.foo(); } } |
package method.command.doubleDispatch; import tool.God; public class Test{ public static void Z双分派(){ Z z1 = (Z)God.create("3-18-Z1");//Z1对象 Z z2 = (Z)God.create("3-18-Z2");//Z1对象 z1.foo(z1); z1.foo(z2); z2.foo(z1); z2.foo(z2); } } |
测试结果:
Z1.foo(Z1)- Z1.m() Z1.foo(Z2)- Z1.m() Z2.foo(Z1)- Z2.m() Z2.foo(Z2)- Z2.m() |
对于消息a.foo(b),假设a、b声明为Z类型变量,目前我们模拟了双分派Double Dispatch。
图2 简洁的双分派结构
这是一种有用的结构,我们可以称之为双分派模式。,是不是很牛逼?
不过,有些讨人嫌的家伙,他们更牛逼,他们在自己的书中,把双分派模式称为访问者模式!
你可以修改上述代码,使得消息a.foo(b),其中a声明为X类型变量、b声明为Z类型变量。
按照模拟的双分派模型,会成为2*2的表示方式,,这是两个步骤的叠加而形成4个处理流程。没有双分派机制,那么区分重载操作是不可行的,对于X1和X2的对象,需要方法fooZ1(Z1)和fooZ2(Z2),与a.foo(b)对应的,有4个方法体。
|