ダブルディスパッチという概念を初めて知ったのでメモ。 オーバーロードと似た概念に見えて全然違う。

ダブルディスパッチ

あるオブジェクトのメソッドを呼び出すときに、 そのオブジェクトの実行時の型とその引数の実行時の型をもとに動的に異なる処理を行うこと。

オーバーロード

あるオブジェクトのメソッドを呼び出すときに、 引数の変数の型(コンパイル時に分かる型)をもとに静的に異なる処理を行うこと。

具体例

この二つの違いは、具体例で見るのが早い。 以下、コードは全てJava。

まず以下のコードを考える。

interface Page {
}

class BlogPage implements Page {
}

class InfoPage implements Page {
}

interface PageScorer {
    void evaluate(BlogPage page);
    void evaluate(InfoPage page);
    void evaluate(Page page);

    int getScore();
}

/**
 * BlogPageとInfoPageを異なる点数で点数付けする
 */
class ConcretePageScorer implements PageScorer {
    private static final int BLOG_SCORE = 2;
    private static final int INFO_SCORE = 1;
    private static final int UNKNOWN_SCORE = 0;
    private int score = 0;

    @Override
    public void evaluate(BlogPage page) {
        this.score += BLOG_SCORE;
    }

    @Override
    public
    void evaluate(InfoPage page) {
        this.score += INFO_SCORE;
    }

    @Override
    public
    void evaluate(Page page) {
        // Unknown page type
        this.score += UNKNOWN_SCORE;
    }

    @Override
    public int getScore() {
        return this.score;
    }
}

public class Main {
    public static void main(String[] args) {
        PageScorer scorer = new ConcretePageScorer();
        BlogPage blogPage = new BlogPage();
        InfoPage infoPage = new InfoPage();

        scorer.evaluate(blogPage);
        scorer.evaluate(infoPage);

        System.out.println(scorer.getScore());
    }
}

これを実行すると、「3」が出力される。 特に、以下の行

scorer.evaluate(blogPage);

では、PageScorerの以下のメソッドを呼び出している。

void evaluate(BlogPage page)

このように、Javaは言語機能としてオーバーロードを備えており、引数の変数の型(ここではBlogPage)に合わせて適切なメソッドを呼び出すことができる。

ところが、main関数の中を以下のように変えるだけで、上記コードで「3」の代わりに「0」が出力されてしまう。

PageScorer scorer = new ConcretePageScorer();
List<Page> pageList = new ArrayList<>();
pageList.add(new BlogPage());
pageList.add(new InfoPage());

for (Page page : pageList) {
    scorer.evaluate(page);
}

System.out.println(scorer.getScore());

これは、page変数の型があくまでPage型であるために、以下の行

scorer.evaluate(page);

で、PageScorerの以下のメソッド

void evaluate(Page page)

が呼び出されてしまうためである。

ダブルディスパッチとは、こののような状況で、page変数の実行時の型(BlogPageやInfoPage)に合わせて動的に呼び出すメソッドを変えることを指す。 Javaは言語機能としてはダブルディスパッチに対応していない

ダブルディスパッチをJavaで実現する方法はいくつか存在する。

一つは、instanceofを使って変数の実行時の型に合わせてif文で処理を分岐する方法。

しかし、instanceofを使わずとも、以下のようにすることで、実際にダブルディスパッチを実現できる(Visitorパターン)。

interface Page {
  void acceptScorer(PageScorer scorer);
}

class BlogPage implements Page {
    @Override
    public void acceptScorer(PageScorer scorer) {
        scorer.evaluate(this);
    }
}

class InfoPage implements Page {
    @Override
    public void acceptScorer(PageScorer scorer) {
        scorer.evaluate(this);
    }
}

//PageScorer interface 及び ConcretePageScorer class には上と変更はないので省略.

public class Main {
    public static void main(String[] args) {
        PageScorer scorer = new ConcretePageScorer();

        List<Page> pageList = new ArrayList<>();
        pageList.add(new BlogPage());
        pageList.add(new InfoPage());

        for (Page page : pageList) {
            page.acceptScorer(scorer);
        }

        System.out.println(scorer.getScore());
    }
}

肝は、BlogPageクラス及びInfoPageクラスに存在する以下の行である。

@Override
public void acceptScorer(PageScorer scorer) {
    scorer.evaluate(this);
}

ここで、scorer.evaluate(this)におけるthisは、BlogPage内ではBlogPage型、InfoPage内ではInfoPage型であるから、オーバーロードによりそれぞれ対応したPageScorerのメソッドが正しく呼ばれる。

上記コードの欠点

上記コードにはいくつか欠点もある。

  1. Pageのサブクラスに毎回同じコードを書かなければならない。
  2. Pageの新しいサブクラスを作ったときの変更が煩わしい。

2について補足する。 たとえば、以下のように新たなQAPageを作ったとしよう。

class QAPage implements Page {
    @Override
    public void acceptScorer(PageScorer scorer) {
        scorer.evaluate(this);
    }
}

このときに、QAPageに対応した新たなPageScorerを作りたい。 そこで、以下のようなクラスを作る。

class ConcretePageScorer2 implements PageScorer{
    private static final int BLOG_SCORE = 1;
    private static final int INFO_SCORE = 1;
    private static final int QA_SCORE = 5;
    private static final int UNKNOWN_SCORE = 0;
    private int score = 0;

    public void evaluate(QAPage page) {
        this.score += QA_SCORE;
    }

        /*
         * 残りのコードはConcretePageScorerと同じなので省略
        */
        ...
}
PageScorer scorer = new ConcretePageScorer2();

List<Page> pageList = new ArrayList<>();
pageList.add(new BlogPage());
pageList.add(new InfoPage());
pageList.add(new QAPage());

for (Page page : pageList) {
    page.acceptScorer(scorer);
}

System.out.println(scorer.getScore());

しかし、上記コードを実行すると「2」と表示され、うまくいかない。 PageScorerインターフェイスにQAPage用のメソッドがないために、オーバーロードの解決時にConcretePageScorer2の以下のメソッドが考慮されないためである。

public void evaluate(QAPage page)

この問題を直接解決するためには、PageScorerインターフェイスにQAPage用のメソッドを新たに追加しなければならない。 しかしそうすると、PageScorerインターフェイスをimplementsしているConcretePageScorerクラスにも、対応したメソッドを追加しなければならない(たとえConcretePageScorerクラスをQAPageに対して使用しないとしても!)。 もしPageScorerの実装クラスが大量にあると、これは大変な手間である。

これに対処するには、全体の構成を以下のように変えればよい。

interface Page<S extends PageScorer> {
  void acceptScorer(S scorer);
}

class BlogPage implements Page<PageScorer> {
    @Override
    public void acceptScorer(PageScorer scorer) {
        scorer.evaluate(this);
    }
}

class InfoPage implements Page<PageScorer> {
    @Override
    public void acceptScorer(PageScorer scorer) {
        scorer.evaluate(this);
    }
}

class QAPage implements Page<NewPageScorer> {
    @Override
    public void acceptScorer(NewPageScorer scorer) {
        scorer.evaluate(this);
    }
}

interface PageScorer {
    void evaluate(Page<?> page);
    void evaluate(BlogPage page);
    void evaluate(InfoPage page);

    int getScore();
}

interface NewPageScorer extends PageScorer {
    void evaluate(QAPage page);
}

/**
 * BlogPageとInfoPageを異なる点数で点数付けする.
   それ以外のPageはもしあっても全て0点.
 */
class ConcretePageScorer implements PageScorer {
    private static final int BLOG_SCORE = 2;
    private static final int INFO_SCORE = 1;
    private static final int UNKNOWN_SCORE = 0;
    private int score = 0;

    @Override
    public void evaluate(BlogPage page) {
        this.score += BLOG_SCORE;
    }

    @Override
    public
    void evaluate(InfoPage page) {
        this.score += INFO_SCORE;
    }
    
    @Override
    public void evaluate(Page<?> page) {
        this.score += UNKNOWN_SCORE;
    }

    @Override
    public int getScore() {
        return this.score;
    }
}

/**
 * BlogPageとInfoPageとQAPageを異なる点数で点数付けする.
   それ以外のPageはもしあっても全て0点.
 */
class ConcretePageScorer2 implements NewPageScorer {
    private static final int BLOG_SCORE = 1;
    private static final int INFO_SCORE = 1;
    private static final int QA_SCORE = 5;
    private static final int UNKNOWN_SCORE = 0;
    private int score = 0;

    @Override
    public void evaluate(BlogPage page) {
        this.score += BLOG_SCORE;
    }

    @Override
    public
    void evaluate(InfoPage page) {
        this.score += INFO_SCORE;
    }

    @Override
    public void evaluate(QAPage page) {
        this.score += QA_SCORE;
    }
    
    @Override
    public void evaluate(Page<?> page) {
        this.score += UNKNOWN_SCORE;
    }

    @Override
    public int getScore() {
        return this.score;
    }
}



public class Main {
    public static void main(String[] args) {
        NewPageScorer scorer = new ConcretePageScorer2();

        List<Page<? super NewPageScorer>> pageList = new ArrayList<>();
        pageList.add(new BlogPage());
        pageList.add(new InfoPage());
        pageList.add(new QAPage());

        for (Page<? super NewPageScorer> page : pageList) {
            page.acceptScorer(scorer);
        }

        System.out.println(scorer.getScore());
    }
}

これにより、Pageのサブクラスの追加が容易になる。 すなわち、Pageのサブクラスを新たにいくつか足したら、 それらに合わせてPageScorerを継承したinterfaceを新たに作れば良い。 過去に作ったPageScorerは一切手を加えずにそのまま使える。

ただしこれだとPageの種類が増えるたびに新しくPageScorerを継承したインターフェイスを作らなければならないので、Pageの追加が頻繁な場合には汚くなってしまう。 あくまで、Pageのサブクラスの追加が滅多に起こりえない(せいぜい1,2回)の場合にのみ使える。

参考: ジェネリクスによるVisitorパターン拡張の考察 - プログラマーの脳みそ