Java : Chain of Responsibility パターン (図解/デザインパターン)
Chain of Responsibility パターンとは、GoF によって定義されたデザインパターンの1つです。
必要に応じて、一連のオブジェクトにリクエストを転送していく、という設計です。
本記事では、Chain of Responsibility パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Chain of Responsibility パターンとは、
- ある一連のオブジェクト A, B, C ... Z がある。
- A に対して処理を依頼(リクエスト) すると、必要に応じて、A → B → C → ... → Z へとリクエストは転送される。
という設計です。
Chain of Responsibility は、日本語的に発音すると「チェイン・オブ・レスポンシビリティ」となります。
意味は「責任の連鎖」ですね。
【Chain of Responsibility パターンのイメージ】
処理の依頼(リクエスト) を受け取る、とあるオブジェクトがあります。
このオブジェクトを「ハンドラ」オブジェクトとしましょう。
最初にリクエストを受け取った「ハンドラA」は、次のオブジェクトに対して
- リクエストを転送するべきか?
を判断する 責任(Responsibility) を持ちます。
もし転送が必要ならば、次の「ハンドラB」へリクエストを転送します。
「ハンドラB」でもリクエスト転送の 責任 を持ちます。
必要に応じて、次の「ハンドラC」へと転送します。
そこでも転送が必要なら、さらに次へ … と 責任(Responsibility) を 連鎖(Chain) させます。
これが Chain of Responsibility パターン のざっくりとしたイメージです。
【補足】
Chain of Responsibility パターンは、Decorator パターン とよく似ています。
ただし、Decorator パターンには
- リクエストを転送するべきか?
を判断する 責任 はありません。
先ほどの図でいうと、Decorator パターンでは、すべての「ハンドラ」で処理が実行されます。
連鎖 の途中で処理が終了することはありません。
関連記事:
クラス図とシーケンス図
上の図は、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!
結果も問題なしですね。
必要に応じて、ハンドラが連鎖することを確認できました。
具体的な例
もう少し具体的な例も見てみましょう。
あるシステムでは、2つのユースケースがあります。
- 自分のユーザ情報を表示
- 全ユーザの情報を表示
自分のユーザ情報を表示するには「名前」と「パスワード」で認証する必要があります。
全ユーザの情報は、管理者権限を持つユーザしか見ることができません。
それでは、このシステムを Chain of Responsibility パターンを使って実現してみましょう。
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 へとリクエストは転送される。
という設計です。
有効に活用していきたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン