北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Visitorパターンで使われている"ダブルディスパッチ"とは?

Wikipediaのダブルディスパッチの解説が、とてもいいこと書いてあるんですがどうもわかりにくかったので、Javaで簡潔に書き直してみました。

概要

メソッドのオーバーロードでは昔以下で書いたように動的なメソッド選択ができません。

オーバーロードコンパイル時にメソッドが選択、つまり、変数の宣言されている型で判断されているようです。

オーバーロードとオーバーライドの混沌

ダブルディスパッチによって、オーバーロードされたメソッドをruntimeに動的に選択できるようになります。

ダブルディスパッチで解決できる問題例

次のようなコードを書くと、適切なexecuteが呼べません(コンパイルできない)。これは、メソッドのオーバロードは静的に評価されるためです。

interface MyClassB {}
class MyClassB1 implements MyClassB {}
class MyClassB2 implements MyClassB {}

class MyClassA{
    public void execute(final MyClassB1 b1){
        System.out.println("execute MyClassB1");
    }
    public void execute(final MyClassB2 b2){
        System.out.println("execute MyClassB2");
    }
}

public class DoubleDispatch {
    static public void main(final String args[]){
        final MyClassA a = new MyClassA();
        final MyClassB b = new MyClassB2();
        a.execute(b);
    }
}

解決方法

オーバーロードと違い、オーバーライドされたメソッド呼び出しは動的に選択されることを利用します。

インスタンスと引数をひっくり返すことにより、オーバーライドされたメソッド群から適切なメソッドを呼び出し、そこで型を確定させます。型が確定できてから、オーバーロードされたメソッドを呼び出します。2段階でメソッドをディスパッチするのでダブルディスパッチと言います。

interface MyClassB {
    public void executedBy(final MyClassA a);
}
class MyClassB1 implements MyClassB {
    public void executedBy(final MyClassA a){
        a.execute(this);
    }
}
class MyClassB2 implements MyClassB {
    public void executedBy(final MyClassA a){
        a.execute(this);
    }
}

class MyClassA{
    public void execute(final MyClassB1 b1){
        System.out.println("execute MyClassB1");
    }
    public void execute(final MyClassB2 b2){
        System.out.println("execute MyClassB2");
    }
}

public class DoubleDispatch {
    static public void main(final String args[]){
        final MyClassA a = new MyClassA();
        final MyClassB b = new MyClassB2();
        b.executedBy(a);
    }
}

MyClassB1 と MyClassB2 内の executedBy() で this を execute() に渡してますが、main()関数内の インスタンスb と違ってこのthisは型が確定していると言うのがポイントです*1

*1:なので、executedBy() がまったく一緒のメソッドだからといって、 インタフェース MyClassB を abstract に変えてこの中に executedBy() を実装しようと言う考えはうまくいかないです。なぜなら、その場合のthisはMyClassBのままなので。