概要
リクエストやイベントを複数のハンドラーに順番に渡し、処理できるハンドラーが対応する。この構造は、ログレベルに応じた出力先の切り替えや、承認ワークフローの段階的なエスカレーションなど、業務システムでも頻繁に現れます。Chain of Responsibility パターンは、処理の連鎖を動的に組み替えられる点が特徴ですが、チェーンが長くなるとデバッグが難しくなる側面もあります。この記事では、ログフィルタリングを題材にパターンの構造を実装し、閾値による振り分けやチェーンの組み立て方を整理します。record によるログデータの不変化、sealed interface による型安全なイベント表現まで、バージョンごとの書き方の違いも確認します。
使いどころ
ログレベル(DEBUG / INFO / WARN / ERROR)に応じてコンソール・ファイル・メール通知の出力先を振り分ける
経費申請の金額に応じて担当者 → 課長 → 部長 → 役員と承認権限をエスカレーションする
入力バリデーションを形式チェック → 桁数チェック → 業務ルールチェックの順に連鎖させ、途中で失敗したら後続をスキップする
コード例
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));
}
}Version Coverage
record でログデータ(LogRecord)を不変オブジェクトとして定義でき、var でチェーン構築が簡潔になる。
// 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
注意点
チェーンの末端までどのハンドラーも処理しなかった場合の挙動を明示しておくこと。デフォルトハンドラーを末尾に置くか、例外を投げるかは設計時に決める
setNext の戻り値を次のハンドラーにすると consoleHandler.setNext(fileHandler).setNext(emailHandler) のようにチェーンで組めるが、先頭ハンドラーの参照を見失いやすいので変数に保持しておく
ハンドラーの順序を間違えると意図しないフィルタリングになる。閾値が高い順に並べるか低い順に並べるかは、処理を通すか止めるかの設計と合わせて検討する
チェーンが循環すると無限ループになる。setNext で自分自身や既にチェーンにいるハンドラーを設定しないようガードを入れることも検討する
FAQ
Chain はリクエストを処理できるハンドラーが担当する構造で、途中で止まることがあります。Decorator は常に元の処理を呼び出したうえで機能を追加する構造で、チェーン全体が必ず実行されます。
handle メソッド内で next.handle() を呼ばなければ後続に渡りません。boolean の戻り値で処理済みかを返す設計も有効です。
Logger に複数の Handler を登録する仕組みは広義の Chain of Responsibility です。ただし標準 API は連鎖ではなくリスト走査で全 Handler に通知する設計です。