Help us understand the problem. What is going on with this article?

Java8 ラムダ式&Streamによるデザインパターン再考 - Chain of Responsibility パターン -

More than 3 years have passed since last update.

はじめに

このシリーズでは、Java8で導入されたラムダ式・Stream APIを利用した、デザインパターンの実装方法を検討していきます。前回の記事はTemplate Methodパターンを取り上げました。
サンプルコードはGitHubに上げています。

今回のパターン

Chain of Responsibility は「責任の連鎖」と訳されますが、その名が示すとおりチェーン状に構成されたオブジェクトが次々と責任を受け渡していくことで問題を解決するパターンです。
クラスの静的構造は以下の図のようになります。

cor.png

実際には ConcreteHandler1のインスタンス -> ConcreteHandler2のインスタンス -> ... -> ConcreteHandlerNのインスタンス というように複数のハンドラがつながった構造となります。
クライアントはチェーンの先頭のハンドラにリクエストを送り、先頭のハンドラは自分で解決できればそのまま結果を返しますが、自分で解決できない問題の場合、次のハンドラへ処理を委譲します。
この流れがチェーンに沿って伝搬し、最終的に問題を解決できたハンドラは処理結果を前のハンドラへ戻し、そのハンドラはさらに前のハンドラへ戻し・・・と繰り返され、最後にはクライアントに処理結果が返されます。
クライアントは、どのハンドラが問題を解決したのか気にする必要はありません。

題材

今回のサンプルは 決裁承認 を題材としました。 課長 -> 部長 -> 事業部長 と役職が上がるにつれて決裁可能な金額が上がるものとし、自分の決裁権限内であれば自分で承認を、そうでなければ上位役職者に承認処理を委譲するというシナリオを想定します。

従来の実装

Chain of Responsibility パターンでは、まず問題に対する責任を担うオブジェクトのチェーンを組み立てる必要があります。
自分で解決できたら結果を返す。そうでなければ委譲先の処理を呼び出す という振る舞いは共通化できるので抽象クラスに定義するのが定石です。また、個々の解決処理は具象クラス毎に異なるため、 Template Method パターンを使用することになるでしょう。
以下のサンプルコードの、 approve(int) がテンプレートメソッド、 doApprove(int) がフックメソッドです。
なお approve(int) 内の2つ目のnull判定は Null Object パターンを用いて回避する場合もありますが、ここでは割愛します。

AbstractApprover.java
public abstract class AbstractApprover {
    private AbstractApprover next;
    protected String name;

    public AbstractApprover(String name, AbstractApprover next) {
        this.name = name;
        this.next = next;
    }

    public final Approval approve(int amount) {
        Approval approval = doApprove(amount);
        if (approval != null) {
            return approval;
        } else if (next != null) {
            return next.approve(amount);
        } else {
            System.out.println(String.format("決裁できません。 %,3d円", amount));
            return null;
        }
    }

    protected abstract Approval doApprove(int amount);
}

サブクラスの実装例は以下のようになります。

Kacho.java
public class Kacho extends AbstractApprover {

    public Kacho(String name, AbstractApprover next) {
        super(name, next);
    }

    @Override
    protected Approval doApprove(int amount) {
        if (amount <= 10000) {
            System.out.println(String.format("課長決裁(%s) %,3d円", name, amount));
            return new Approval(name, amount);
        } else {
            return null;
        }
    }
}

チェーンの組み立て、実行は以下のようになります。(テストコードのため自分で組み立ててますが、実際には Factory から取得するでしょう)

Usage
        JigyoBucho jigyoBucho = new JigyoBucho("山田", null);
        Bucho bucho = new Bucho("田中", jigyoBucho);
        Kacho kacho = new Kacho("鈴木", bucho);
        Approval approval = kacho.approve(10000);
        assertEquals("鈴木", approval.getApprover());
        approval = kacho.approve(100000);
        assertEquals("田中", approval.getApprover());
        approval = kacho.approve(1000000);
        assertEquals("山田", approval.getApprover());
        approval = kacho.approve(1000001);
        assertNull(approval);

ラムダ式を用いた実装方法

今回の例のような、金額などの条件判断のみの差異をいちいちサブクラスを定義して実装するのは面倒なため、ラムダ式で表現するようにし、合わせてチェーンの組み立てを行うメソッドも用意することにします。
Java8からインターフェースにdefaultメソッドとstaticメソッドを定義可能になったので、以下のように1つのインターフェースの中に定義することができます。

Approvable.java
@FunctionalInterface
public interface Approvable {

    Approval approve(int amount);

    default Approvable delegate(Approvable next) {
        return (amount) -> {
            final Approval result = approve(amount);
            return result != null ? result : next.approve(amount);
        };
    }

    static Approvable chain(Approvable... approvables) {
        return Stream.concat(Stream.of(approvables), Stream.of(tail()))
                .reduce((approvable, next) -> approvable.delegate(next))
                .orElse(null);
    }

    static Approvable approver(String title, String name, Predicate<Integer> predicate) {
        return amount -> {
            if (predicate.test(amount)) {
                System.out.println(String.format("%s決裁(%s) %,3d円", title, name, amount));
                return new Approval(name, amount);
            } else {
                return null;
            }
        };
    }

    static Approvable tail() {
        return amount -> {
            System.out.println(String.format("決裁できません。 %,3d円", amount));
            return null;
        };
    }

}

以下、上記コードの中身を解説します。

まず、 与えられた金額を決済する 責務は、金額を受け取り承認結果を返す delegate(int):Approval メソッドとして定義しています。
この処理を課長や部長などの役職ごとにラムダ式で実装するため、この処理を唯一のメソッドとして持つ関数型インターフェースとして定義し、 @FunctionalInterface アノテーションを付けます。

@FunctionalInterface
public interface Approvable {

    Approval approve(int amount);

次のdefaultメソッド delegate(Approvable) は関数合成を行うために用意したメソッドです。
java.util.Function<T, R> には composeandThen という関数合成用のメソッドが用意されているので、単純に処理をつなげていくならこれらを用いることが可能です。
しかし、 Chain of Responsibility パターンの場合は、 自分が解決できればその時点で処理の伝搬を中止する 必要があるため、自前でメソッドを用意してあげる必要があります。
最初に自分自身の処理( approve )を呼び出し、結果がnullならば次の処理を呼び出すという振る舞いをラムダ式で返します。

    default Approvable delegate(Approvable next) {
        return (amount) -> {
            final Approval result = approve(amount);
            return result != null ? result : next.approve(amount);
        };
    }

その次のstaticメソッド chain(Approvable...) はチェーンの組み立てを行います。
可変長引数で受け取った Approvable の配列から Stream を形成して、reduceすることで1つの関数に合成して返しています。この際に先程の delegate メソッドを使用します。

    static Approvable chain(Approvable... approvables) {
        return Stream.concat(Stream.of(approvables), Stream.of(tail()))
                .reduce((approvable, next) -> approvable.delegate(next))
                .orElse(null);
    }

次の approver(String, String, Predicate<Integer>) は実際の Approvable の実装(課長や部長にあたるもの)を生成するための Factory Method です。

    static Approvable approver(String title, String name, Predicate<Integer> predicate) {
        return amount -> {
            if (predicate.test(amount)) {
                System.out.println(String.format("%s決裁(%s) %,3d円", title, name, amount));
                return new Approval(name, amount);
            } else {
                return null;
            }
        };
    }

Approvable を使用したチェーンの組み立てと処理実行は以下の例のようになります。

Usage
        Approvable kacho = Approvable.approver("課長", "鈴木", amount -> amount <= 10000);
        Approvable bucho = Approvable.approver("部長", "田中", amount -> amount <= 100000);
        Approvable jigyoBucho = Approvable.approver("事業部長", "山田", amount -> amount <= 1000000);

        Approvable cor = Approvable.chain(kacho, bucho, jigyoBucho);
        Approval approval = cor.approve(10000);
        assertEquals("鈴木", approval.getApprover());
        approval = cor.approve(100000);
        assertEquals("田中", approval.getApprover());
        approval = cor.approve(1000000);
        assertEquals("山田", approval.getApprover());
        approval = cor.approve(1000001);
        assertNull(approval);

発展:汎用化してみる

総称型をうまく使えば、 Chain of Responsiblity の性質を汎用化して定義することができます。
ここではその性質を以下のように定義し、汎用化を試みます。

  • 個々の要素は型Qのデータを受け取って処理し、型Rの結果を返す
  • 自分自身が解決できない(型Rの結果を返せない)場合は次の要素に処理を委譲する
  • この処理委譲はチェーンの末端に向かって連鎖的に伝搬し、最終的に解決された結果がクライアントに返される

前節の Approvable に該当する関数型インターフェースを以下のように定義します。
型パラメータが入ってくるので若干読みづらくなりますが、処理内容は同等です。

Delegatable.java
@FunctionalInterface
public interface Delegatable<Q, R> {

    R call(Q q);

    default Delegatable<Q, R> delegate(Delegatable<Q, R> next) {
        return q -> {
            final R result = call(q);
            return result != null ? result : next.call(q);
        };
    }
}

そして、前述の性質を実装するクラスは、まさに ChainOfResponsibility<Q, R> と名付けました。

ChainOfResponsibility
public class ChainOfResponsibility<Q, R> {
    private List<Delegatable<Q, R>> chain;

    public ChainOfResponsibility() {
        super();
        chain = new ArrayList<>();
    }

    public ChainOfResponsibility<Q, R> add(Function<Q, R> func) {
        chain.add(q -> func.apply(q));
        return this;
    }

    public ChainOfResponsibility<Q, R> tail(Consumer<Q> tail) {
        chain.add(q -> {
            tail.accept(q);
            return null;
        });
        return this;
    }

    public Function<Q, R> build() {
        Optional<Delegatable<Q, R>> head = chain.stream()
                .reduce((delegatable, next) -> delegatable.delegate(next));
        return head.isPresent() ? q -> head.get().call(q) : null;
    }

}

使用例は以下となります。

Usage
    @Test
    public void test() {
        Function<Integer, Approval> kacho = approver("課長", "鈴木", amount -> amount <= 10000);
        Function<Integer, Approval> bucho = approver("部長", "田中", amount -> amount <= 100000);
        Function<Integer, Approval> jigyoBucho = approver("事業部長", "山田", amount -> amount <= 1000000);

        Function<Integer, Approval> head = new ChainOfResponsibility<Integer, Approval>()
                .add(kacho)
                .add(bucho)
                .add(jigyoBucho)
                .tail(amount -> System.out.println(String.format("決裁できません。 %,3d円", amount)))
                .build();

        Approval approval = head.apply(10000);
        assertEquals("鈴木", approval.getApprover());
        approval = head.apply(100000);
        assertEquals("田中", approval.getApprover());
        approval = head.apply(1000000);
        assertEquals("山田", approval.getApprover());
        approval = head.apply(1000001);
        assertNull(approval);

    }

    private Function<Integer, Approval> approver(String title, String name, Predicate<Integer> predicate) {
        return amount -> {
            if (predicate.test(amount)) {
                System.out.println(String.format("%s決裁(%s) %,3d円", title, name, amount));
                return new Approval(name, amount);
            } else {
                return null;
            }
        };
    }

まとめ

Chain of Responsibility パターンにおける処理の伝搬は、関数合成を行うことでうまく実装できました。
また、総称型を使うことでパターンの汎用化も可能です。ラムダ式による関数型プログラミングは非常に強力ですね。

yonetty
某SIerでアーキテクトとしてエンタープライズ向けシステム・製品の開発に携わっています。 Twitter: @tyonekubo
https://blog.ynkb.xyz/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした