DP-13: Chain of Responsibility パターン
リクエストを受け取ったハンドラーが処理できる場合はそのまま処理し、 処理できない場合は次のハンドラーに渡すパターンです。 送信者と受信者の結合度(依存の強さ)を下げ、処理の流れを柔軟に変更できます。
Chain of Responsibility パターンとは
「このリクエストは誰が処理するのか」を送信者が知らなくてもよいようにするパターンです。 複数のハンドラーをチェーン(鎖)状につなぎ、 リクエストを先頭のハンドラーから順に渡していきます。 各ハンドラーは「自分が処理できるか」を判断し、処理できれば対応し、 できなければ次のハンドラーに渡します(または両方行います)。
Java 標準ライブラリの実例
- java.util.logging の Handler チェーン:
Loggerに複数のHandler(ConsoleHandler・FileHandler など)を追加すると、 ログレコードが各 Handler に順に渡されます - Servlet フィルターチェーン:
FilterChain.doFilter()を呼ぶことで次のフィルターまたはサーブレットにリクエストを渡します。doFilter()を呼び忘れるとリクエストが止まります
今回のサンプルはログレベルのフィルタリングチェーンです。 DEBUG → INFO → WARN → ERROR の優先度があり、 各ハンドラーは自分の閾値(しきいち)以上のログだけを処理します。 WARN レベルのログは ConsoleHandler が処理し、 ERROR レベルのログは ConsoleHandler・FileHandler・EmailHandler の全てが処理します。
各ハンドラーの処理範囲
ログレベル ConsoleHandler FileHandler EmailHandler
(WARN 以上) (ERROR 以上) (ERROR 以上)
DEBUG → スキップ スキップ スキップ
INFO → スキップ スキップ スキップ
WARN → 処理する スキップ スキップ
ERROR → 処理する 処理する 処理するサンプルコード
Java 8 ではテンプレートメソッドパターンと組み合わせて、handle() メソッドに「チェーンを継続する」ロジックを集約します。サブクラスは writeLog() だけを実装すれば、チェーンの継続は自動的に行われます。
よくあるミス・注意点
⚠️ next.handle() を呼び忘れてリクエストが途中で止まる
Servlet のフィルターチェーンでは chain.doFilter() を、 今回のサンプルでは next.handle() を呼ばないと チェーンがそこで止まります。 後続のハンドラーが呼ばれないため「処理されたはずのログが記録されない」 「認証フィルターを通過できない」などのバグにつながります。 本サンプルでは handle() を final にして サブクラスが誤って next の呼び出しを省略できないようにしています。
⚠️ チェーンに終端がなく無限ループになる
ハンドラーが誤って自分自身を next に設定したり、 循環するようにチェーンを組むと StackOverflowError が発生します。setNext() で自分自身が渡されていないかチェックする防御コードを入れると安全です。
⚠️ チェーンの順序を間違える
ハンドラーを登録する順序によって動作が変わります。 認証・認可のフィルターでは「認証チェックを先に行い、認証済みの場合だけ認可チェックを行う」 という順序が正しいです。順序が逆だと認証前のユーザーに認可チェックが走り、 セキュリティ上の問題になることがあります。
テストする観点
- 各ハンドラーが自分の閾値以上のログを処理すること(
ConsoleHandlerは WARN 以上) - 各ハンドラーが自分の閾値未満のログをスキップすること(境界値: 閾値より1レベル低いログ)
- ERROR ログがチェーンの末端(EmailHandler)まで伝播すること
- DEBUG / INFO ログがどのハンドラーにも処理されないこと
- チェーンが1ハンドラーだけの場合(最小構成)でも正しく動作すること
- next に null が設定されているとき(チェーンの末端)に NullPointerException が発生しないこと