概要

リクエストやイベントを複数のハンドラーに順番に渡し、処理できるハンドラーが対応する。この構造は、ログレベルに応じた出力先の切り替えや、承認ワークフローの段階的なエスカレーションなど、業務システムでも頻繁に現れます。Chain of Responsibility パターンは、処理の連鎖を動的に組み替えられる点が特徴ですが、チェーンが長くなるとデバッグが難しくなる側面もあります。この記事では、ログフィルタリングを題材にパターンの構造を実装し、閾値による振り分けやチェーンの組み立て方を整理します。record によるログデータの不変化、sealed interface による型安全なイベント表現まで、バージョンごとの書き方の違いも確認します。

使いどころ

ログレベル(DEBUG / INFO / WARN / ERROR)に応じてコンソール・ファイル・メール通知の出力先を振り分ける

経費申請の金額に応じて担当者 → 課長 → 部長 → 役員と承認権限をエスカレーションする

入力バリデーションを形式チェック → 桁数チェック → 業務ルールチェックの順に連鎖させ、途中で失敗したら後続をスキップする

コード例

ChainOfResponsibilityDemo.java
import java.time.LocalDateTime;

public class ChainOfResponsibilityDemo {

    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; }
    }

    record LogRecord(LogLevel level, String message,
                     LocalDateTime timestamp) {
        public String format() {
            return "[" + timestamp + "][" + level + "] " + message;
        }
    }

    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;
        }

        public final void handle(LogRecord record) {
            if (record.level().getLevel() >= threshold.getLevel()) {
                writeLog(record);
            }
            if (next != null) {
                next.handle(record);
            }
        }

        protected abstract void writeLog(LogRecord record);
    }

    static class ConsoleHandler extends LogHandler {
        public ConsoleHandler() { super(LogLevel.WARN); }
        @Override
        protected void writeLog(LogRecord record) {
            System.out.println("[CONSOLE] " + record.format());
        }
    }

    static class FileHandler extends LogHandler {
        public FileHandler() { super(LogLevel.ERROR); }
        @Override
        protected void writeLog(LogRecord record) {
            System.out.println("[FILE] " + record.format());
        }
    }

    static class EmailHandler extends LogHandler {
        public EmailHandler() { super(LogLevel.ERROR); }
        @Override
        protected void writeLog(LogRecord record) {
            System.out.println("[EMAIL] " + record.format());
        }
    }

    public static void main(String[] args) {
        var console = new ConsoleHandler();
        console.setNext(new FileHandler())
               .setNext(new EmailHandler());

        var now = LocalDateTime.now();
        console.handle(new LogRecord(LogLevel.DEBUG, "接続確立", now));
        console.handle(new LogRecord(LogLevel.WARN, "ディスク80%超過", now));
        console.handle(new LogRecord(LogLevel.ERROR, "DB接続失敗", now));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でログデータ(LogRecord)を不変オブジェクトとして定義でき、var でチェーン構築が簡潔になる。

Java 17
// Java 17: record でログデータを不変オブジェクト化
record LogRecord(LogLevel level, String message,
                 LocalDateTime timestamp) {}

public final void handle(LogRecord record) {
    if (record.level().getLevel() >= threshold.getLevel()) {
        writeLog(record);
    }
    if (next != null) { next.handle(record); }
}

Library Comparison

標準 API(abstract class チェーン)ハンドラーの数が少なく、処理の流れが直線的な場面。依存なしで完結する。チェーンの動的な組み替えが必要になると管理コードが増える。
Spring HandlerInterceptorWeb アプリケーションのリクエスト処理パイプラインで、認証・ログ・権限チェックを連鎖させる場合。Spring 依存になるため、バッチやライブラリ単体では使えない。
Jakarta Servlet FilterServlet ベースの Web アプリで、リクエスト/レスポンスのフィルタリングを行う場合。Servlet コンテナ前提のため、非 Web のユースケースには適用できない。

注意点

チェーンの末端までどのハンドラーも処理しなかった場合の挙動を明示しておくこと。デフォルトハンドラーを末尾に置くか、例外を投げるかは設計時に決める

setNext の戻り値を次のハンドラーにすると consoleHandler.setNext(fileHandler).setNext(emailHandler) のようにチェーンで組めるが、先頭ハンドラーの参照を見失いやすいので変数に保持しておく

ハンドラーの順序を間違えると意図しないフィルタリングになる。閾値が高い順に並べるか低い順に並べるかは、処理を通すか止めるかの設計と合わせて検討する

チェーンが循環すると無限ループになる。setNext で自分自身や既にチェーンにいるハンドラーを設定しないようガードを入れることも検討する

FAQ

Chain of Responsibility と Decorator はどう違いますか。

Chain はリクエストを処理できるハンドラーが担当する構造で、途中で止まることがあります。Decorator は常に元の処理を呼び出したうえで機能を追加する構造で、チェーン全体が必ず実行されます。

チェーンの途中でリクエストを止めたい場合はどう実装しますか。

handle メソッド内で next.handle() を呼ばなければ後続に渡りません。boolean の戻り値で処理済みかを返す設計も有効です。

java.util.logging の Handler もこのパターンですか。

Logger に複数の Handler を登録する仕組みは広義の Chain of Responsibility です。ただし標準 API は連鎖ではなくリスト走査で全 Handler に通知する設計です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます