您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
Java模拟 双分派Double Dispatch
 
作者 yqj2065的博客,火龙果软件    发布于 2014-09-05
   次浏览      
 

本节应用命令模式,在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个方法体。

   
次浏览       
相关文章

为什么要做持续部署?
剖析“持续交付”:五个核心实践
集成与构建指南
持续集成工具的选择-装载
 
相关文档

持续集成介绍
使用Hudson持续集成
持续集成之-依赖管理
IPD集成产品开发管理
相关课程

配置管理、日构建与持续集成
软件架构设计方法、案例与实践
单元测试、重构及持续集成
基于Android的单元、性能测试
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

重构-使代码更简洁优美
Visitor Parttern
由表及里看模式
设计模式随笔系列
深入浅出设计模式-介绍
.NET中的设计模式
更多...   

相关培训课程

J2EE设计模式和性能调优
应用模式设计Java企业级应用
设计模式原理与应用
J2EE设计模式指南
单元测试+重构+设计模式
设计模式及其CSharp实现


某电力公司 设计模式原理
蓝拓扑 设计模式原理及应用
卫星导航 UML & OOAD
汤森路透研发中心 UML& OOAD
中达电通 设计模式原理
西门子 嵌入式设计模式
更多...