Java でダブルディスパッチを使ったちょっとした例

今朝 Twitter で Visitor パターンによるダブルディスパッチの話題を見かけたので、そういや昔 Java でちろっと使ったことあったなーとどんなんだったか思い出してみました。

要件

確かこんなのを表現したかったんです。業務アプリでよくありがちな、業務実行可否判定ですね。実際にはもっとパターンが多くなると思います。

業務 エライ人 ヒラ
申請 不可
承認 不可

実装

とりあえずベタに書くとこんなんでしょうか。

// 役割
public enum Role {
    エライ, ヒラ;
}

// 業務
public interface Work {
    boolean isExecutableBy(Role role);
}

業務を実装するとこんな感じ。

// 申請
public class Request implements Work {
    @Override
    public boolean isExecutableBy(Role role) {
        switch (role) {
            case エライ:
                return false;
            case ヒラ:
                return true;
        }
        throw new IllegalArgumentException("定義されてないroleや!!" + role);
    }
}
// 承認
public class Approval implements Work {
    // 略(申請と同じように実装する)
}

要件はみたせてますが switch 分岐で漏れがあると例外がすっとびますね。業務や役割が増えた場合いちいち全部 switch 分岐を直す必要があるので、どこかで実装漏れが起きそうです。

そこでダブルディスパッチ

Role の実装を以下のように変えてみます。業務の具象クラスを引数に取るメソッドをひとつひとつ用意します。

public enum Role {

    エライ {

            @Override
            public boolean canExecute(Request request) {
                return false;
            }

            @Override
            public boolean canExecute(Approval approval) {
                return true;
            }

        }, ヒラ {

            @Override
            public boolean canExecute(Request request) {
                return true;
            }

            @Override
            public boolean canExecute(Approval approval) {
                return false;
            }

        };

    public abstract boolean canExecute(Request request);

    public abstract boolean canExecute(Approval approval);
}

業務の実装はこうなります。判定処理が役割に移動したのですっきりしました。

public class Request implements Work {

    @Override
    public boolean isExecutableBy(Role role) {
        return role.canExecute(this);
    }

}

switch による分岐がなくなり、業務が増えた場合は Role に abstract method を追加することになるのでパターンの漏れをコンパイルエラーという形で検知することができるようになります。業務と役割の関係が一箇所にまとまるので見通しもよくなるような気もします。

ややテクニカルではありますが、こういう二次元表を表現したい場合なんかには使いでがあるのではないかと思います。この例では boolean を返すようにしていますが、戻り値を enumEnumSet などにすることでさらに拡張も考えられます。