広告

Java : Chain of Responsibility パターン (図解/デザインパターン)

Chain of Responsibility パターンとは、GoF によって定義されたデザインパターンの1つです。
必要に応じて、一連のオブジェクトにリクエストを転送していく、という設計です。

本記事では、Chain of Responsibility パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Chain-of-responsibility パターン, CoR パターンは、オブジェクト指向設計におけるデザインパターンの一つであり、一つのコマンドオブジェクトと一連の処理オブジェクト (processing objects) から構成される。

Chain of Responsibility パターンとは、

  • ある一連のオブジェクト A, B, C ... Z がある。
  • A に対して処理を依頼(リクエスト) すると、必要に応じて、A → B → C → ... → Z へとリクエストは転送される。

という設計です。

Chain of Responsibility は、日本語的に発音すると「チェイン・オブ・レスポンシビリティ」となります。
意味は「責任の連鎖」ですね。


【Chain of Responsibility パターンのイメージ】

イメージ図2

処理の依頼(リクエスト) を受け取る、とあるオブジェクトがあります。
このオブジェクトを「ハンドラ」オブジェクトとしましょう。


イメージ図1

最初にリクエストを受け取った「ハンドラA」は、次のオブジェクトに対して

  • リクエストを転送するべきか?

を判断する 責任(Responsibility) を持ちます。
もし転送が必要ならば、次の「ハンドラB」へリクエストを転送します。

「ハンドラB」でもリクエスト転送の 責任 を持ちます。
必要に応じて、次の「ハンドラC」へと転送します。

そこでも転送が必要なら、さらに次へ … と 責任(Responsibility) を 連鎖(Chain) させます。

これが Chain of Responsibility パターン のざっくりとしたイメージです。


【補足】

Chain of Responsibility パターンは、Decorator パターン とよく似ています。
ただし、Decorator パターンには

  • リクエストを転送するべきか?

を判断する 責任 はありません。

先ほどの図でいうと、Decorator パターンでは、すべての「ハンドラ」で処理が実行されます。
連鎖 の途中で処理が終了することはありません。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Chain of Responsibility パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。

まずは Handler クラスです。
フィールドに、次の Handler オブジェクトへのリンクを持ちます。

public class Handler {
    private final Handler next;

    public Handler(Handler next) {
        this.next = next;
    }

    public void handle() {
        if (next != null) {
            next.handle();
        }
    }
}

次に、Handler クラスのサブクラスです。
ConcreteHandlerB については、forwarding フィールドによって、処理を転送するかどうか判断します。

public class ConcreteHandlerA extends Handler {

    public ConcreteHandlerA() {
        super(null);
    }

    @Override
    public void handle() {
        System.out.println("A : Handle!");
    }
}

public class ConcreteHandlerB extends Handler {
    private final boolean forwarding;

    public ConcreteHandlerB(Handler next, boolean forwarding) {
        super(next);
        this.forwarding = forwarding;
    }

    @Override
    public void handle() {
        if (forwarding) {
            System.out.print("B -> ");
            super.handle();
        } else {
            System.out.println("B : Handle!");
        }
    }
}

それでは、これらのクラスを使ってみましょう。

final var handlerA = new ConcreteHandlerA();
handlerA.handle();

// 結果
// ↓
//A : Handle!

final var handlerB1 = new ConcreteHandlerB(handlerA, false);
handlerB1.handle();

// 結果
// ↓
//B : Handle!

final var handlerB2 = new ConcreteHandlerB(handlerA, true);
handlerB2.handle();

// 結果
// ↓
//B -> A : Handle!

結果も問題なしですね。
必要に応じて、ハンドラが連鎖することを確認できました。


具体的な例

もう少し具体的な例も見てみましょう。

アクティビティ図1

あるシステムでは、2つのユースケースがあります。

  1. 自分のユーザ情報を表示
  2. 全ユーザの情報を表示

自分のユーザ情報を表示するには「名前」と「パスワード」で認証する必要があります。
全ユーザの情報は、管理者権限を持つユーザしか見ることができません。

それでは、このシステムを Chain of Responsibility パターンを使って実現してみましょう。

クラス図2

User クラスは、1ユーザの情報を表します。

  • 名前
  • パスワード
  • 年齢

を持ちます。
(レコードクラス を使っています)

※今回の例では、全体的にパスワードは雑に扱っています。
 本来であればもう少し厳格に管理したほうがよいでしょう。

public record User(String name, String password, int age) {
}

UserManager クラスは、ユーザの追加と取得だけの役割です。

public class UserManager {
    private final Map<String, User> users = new HashMap<>();

    public void addUser(User user) {
        users.put(user.name(), user);
    }

    public User getUser(String name) {
        return users.get(name);
    }

    public Collection<User> getAllUsers() {
        return users.values();
    }
}

次に Request クラスです。

  • 名前
  • パスワード

を持ちます。
主にユーザ認証で使います。

public record Request(String name, String password) {
}

Handler クラスのフィールドには、次の Handler オブジェクトへのリンクを持ちます。

public class Handler {
    private final Handler next;

    public Handler(Handler next) {
        this.next = next;
    }

    public void handle(Request request) {
        if (next != null) {
            next.handle(request);
        }
    }
}

AuthHandler クラスは、ユーザ認証を行います。
名前とパスワードが一致したら「認証成功」で、次の Handler オブジェクトへリクエストを継続します。

public class AuthHandler extends Handler {
    private final UserManager userManager;

    public AuthHandler(Handler next, UserManager userManager) {
        super(next);
        this.userManager = userManager;
    }

    @Override
    public void handle(Request request) {
        final var user = userManager.getUser(request.name());
        if (user.password().equals(request.password())) {
            System.out.println("認証成功");
            super.handle(request);
        } else {
            System.out.println("認証失敗");
        }
    }
}

AdminHandler クラスは、ユーザが管理者権限か確認します。
名前が "admin" であれば管理者として、次の Handler オブジェクトへリクエストを継続します。

public class AdminHandler extends Handler {
    public AdminHandler(Handler next) {
        super(next);
    }

    @Override
    public void handle(Request request) {
        if ("admin".equals(request.name())) {
            System.out.println("管理者権限 OK");
            super.handle(request);
        } else {
            System.out.println("管理者権限ではありません");
        }
    }
}

PrintUserHandler クラスは、自分のユーザ情報を表示します。
PrintAllUsersHandler クラスは、全ユーザの情報を表示します。

public class PrintUserHandler extends Handler {
    private final UserManager userManager;

    public PrintUserHandler(UserManager userManager) {
        super(null);
        this.userManager = userManager;
    }

    @Override
    public void handle(Request request) {
        final var user = userManager.getUser(request.name());
        System.out.printf("ユーザ情報 : %s(%d)%n", user.name(), user.age());
    }
}

public class PrintAllUsersHandler extends Handler {
    private final UserManager userManager;

    public PrintAllUsersHandler(UserManager userManager) {
        super(null);
        this.userManager = userManager;
    }

    @Override
    public void handle(Request request) {
        for (final var user : userManager.getAllUsers()) {
            System.out.printf("ユーザ情報 : %s(%d)%n", user.name(), user.age());
        }
    }
}

それでは、これらのクラスを使ってみましょう。

まずは1つめのユースケース

  • 自分のユーザ情報を表示

です。
1回目の hanlde メソッドの呼び出しでは、意図的に認証に失敗しています。

// ------------
// ユーザ追加
final var userManager = new UserManager();
userManager.addUser(new User("admin", "pass_123", -1));
userManager.addUser(new User("alice", "abc", 20));
userManager.addUser(new User("bob", "xyz", 30));

// ------------
// 自分のユーザ情報を表示
final var handler =
        new AuthHandler(
                new PrintUserHandler(userManager),
                userManager
        );

handler.handle(new Request("alice", "456"));

// 結果
// ↓
//認証失敗

handler.handle(new Request("alice", "abc"));

// 結果
// ↓
//認証成功
//ユーザ情報 : alice(20)

次に

  • 全ユーザの情報を表示

のユースケースです。
1回目の hanlde メソッドの呼び出しでは、意図的に管理者権限のないユーザにしています。

// ------------
// ユーザ追加
final var userManager = new UserManager();
userManager.addUser(new User("admin", "pass_123", -1));
userManager.addUser(new User("alice", "abc", 20));
userManager.addUser(new User("bob", "xyz", 30));

// ------------
// 全ユーザの情報を表示
final var handler =
        new AuthHandler(
                new AdminHandler(
                        new PrintAllUsersHandler(userManager)
                ),
                userManager
        );

handler.handle(new Request("alice", "abc"));

// 結果
// ↓
//認証成功
//管理者権限ではありません

handler.handle(new Request("admin", "pass_123"));

// 結果
// ↓
//認証成功
//管理者権限 OK
//ユーザ情報 : bob(30)
//ユーザ情報 : admin(-1)
//ユーザ情報 : alice(20)

結果も問題なしですね。
Handler オブジェクトの組み合わせを変えて、2つのユースケースが実現できました。

もし新規にユースケースを追加する場合も、既存のクラスの修正は必要ありません。
例えば、シニア(高齢者) ユーザだけを対象にしたユースケースなら、

  • SeniorHandler クラス

追加 すれば OK です。

これは 開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

にも則していますね。
Chain of Responsibility パターンのメリットの1つです。

まとめ

Chain of Responsibility パターンとは、

  • ある一連のオブジェクト A, B, C ... Z がある。
  • A に対して処理を依頼(リクエスト) すると、必要に応じて、A → B → C → ... → Z へとリクエストは転送される。

という設計です。

有効に活用していきたいですね。


関連記事

ページの先頭へ