java-recipes

ホーム デザインパターン › DP-13

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    →   処理する        処理する      処理する

サンプルコード

ChainOfResponsibilitySample.java
public class ChainOfResponsibilitySample {

    // ログレベルを表す enum(数値が大きいほど重要度が高い)
    enum LogLevel {
        DEBUG(1),
        INFO(2),
        WARN(3),
        ERROR(4);

        private final int level; // 優先度(数値)

        LogLevel(int level) {
            this.level = level;
        }

        public int getLevel() {
            return level;
        }
    }

    // ログハンドラーの抽象基底クラス
    // テンプレートメソッドパターンと組み合わせて「いつ処理するか」の判断を共通化する
    static abstract class LogHandler {
        private final LogLevel threshold; // この閾値以上のログを処理する
        private LogHandler next;          // 次のハンドラー(チェーンの次のリンク)

        public LogHandler(LogLevel threshold) {
            this.threshold = threshold;
        }

        // 次のハンドラーを設定する(メソッドチェーンで書けるよう自分自身を返す)
        public LogHandler setNext(LogHandler next) {
            this.next = next;
            return next;
        }

        // ログを処理する(テンプレートメソッド)
        // このメソッドは final にして、サブクラスが呼び出しの流れを変えられないようにする
        public final void handle(LogLevel level, String message) {
            if (level.getLevel() >= threshold.getLevel()) {
                // 自分の閾値以上のログは writeLog で処理する
                writeLog(message);
            }
            if (next != null) {
                // 次のハンドラーにも渡す(チェーンを継続する)
                next.handle(level, message);
            }
        }

        // サブクラスが実装する実際のログ出力メソッド
        protected abstract void writeLog(String message);
    }

    // WARN 以上のログをコンソールに出力するハンドラー
    static class ConsoleHandler extends LogHandler {
        public ConsoleHandler() {
            super(LogLevel.WARN);
        }

        @Override
        protected void writeLog(String message) {
            System.out.println("[CONSOLE] " + message);
        }
    }

    // ERROR 以上のログをファイルに出力するハンドラー(本サンプルでは System.out で代替)
    static class FileHandler extends LogHandler {
        public FileHandler() {
            super(LogLevel.ERROR);
        }

        @Override
        protected void writeLog(String message) {
            // 実際のアプリケーションではファイルに書き込む
            System.out.println("[FILE] error.log に書き込み: " + message);
        }
    }

    // ERROR 以上のログをメール通知するハンドラー(本サンプルでは System.out で代替)
    static class EmailHandler extends LogHandler {
        public EmailHandler() {
            super(LogLevel.ERROR);
        }

        @Override
        protected void writeLog(String message) {
            // 実際のアプリケーションではメール送信処理を行う
            System.out.println("[EMAIL] 管理者にメール送信: " + message);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Chain of Responsibility パターン: ログフィルタリング ===");

        // チェーンを構築する: ConsoleHandler -> FileHandler -> EmailHandler
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler
                .setNext(new FileHandler())
                .setNext(new EmailHandler());

        System.out.println("--- DEBUG ログ(閾値未満: どのハンドラーも処理しない)---");
        consoleHandler.handle(LogLevel.DEBUG, "デバッグ情報: 接続確立");

        System.out.println("\n--- INFO ログ(閾値未満: どのハンドラーも処理しない)---");
        consoleHandler.handle(LogLevel.INFO, "情報: ユーザーがログインしました");

        System.out.println("\n--- WARN ログ(ConsoleHandler が処理)---");
        consoleHandler.handle(LogLevel.WARN, "警告: ディスク使用率が 80% を超えました");

        System.out.println("\n--- ERROR ログ(全ハンドラーが処理)---");
        consoleHandler.handle(LogLevel.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 が発生しないこと

GitHub でソースコードを見る →