Javaを鍛える|デザインパターン全23回 #14

Chain of Responsibilityパターン|承認フローの分岐地獄を解消する

  • Java
  • デザインパターン
  • Chain of Responsibility
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before: if-elseの分岐地獄
  3. After: Chain of Responsibilityによる連鎖化
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftの承認フローは、担当者→リーダー→マネージャーの順で承認を進める仕組みになっています。ところが今のコードは、優先度ごとの承認ルートをif-elseとswitch-caseの羅列で表現しており、新しい承認者(例えば「シニアエンジニア」)を1人追加するだけで、複数のメソッドに手を入れてテストをやり直す羽目になっています。

このパターンは何のためにあるか

Chain of Responsibilityは、リクエストの処理者を複数用意し、順次次の処理者に委譲していくことで、単一のオブジェクトに依存せず柔軟に承認フローを組めるようにするデザインパターンです。日常で言えば、受付→担当者が確認→必要に応じて課長へエスカレーション…という窓口業務を想像すると分かりやすいでしょう。

Before: if-elseの分岐地獄

以下は現在のTicketCraftの承認処理コード(抜粋)です。processApproval()メソッド内で優先度ごとにIF文が並んでいます。

import java.util.ArrayList;
import java.util.List;

public class Ticket {
    private String id;
    private int priority;
    private boolean approvedByManager;

    public Ticket(String id, int priority) {
        this.id = id;
        this.priority = priority;
        this.approvedByManager = false;
    }

    public String getId() { return id; }
    public int getPriority() { return priority; }
    public boolean isApprovedByManager() { return approvedByManager; }
}

public class Approver {
    private String role;

    public Approver(String role) { this.role = role; }
    public String getRole() { return role; }
}

public class BeforeMain {
    // 承認フローをIF文で分岐する
    public static boolean processApproval(Ticket ticket, List<Approver> approvers) {
        if (ticket.getPriority() == 0) {
            return processForLowPriority(ticket, approvers);
        } else if (ticket.getPriority() == 1) {
            return processForMediumPriority(ticket, approvers);
        } else if (ticket.getPriority() == 2) {
            return processForHighPriority(ticket, approvers);
        } else {
            throw new IllegalArgumentException("無効な優先度: " + ticket.getPriority());
        }
    }

    private static boolean processForLowPriority(Ticket ticket, List<Approver> approvers) {
        for (Approver a : approvers) {
            if ("担当者".equals(a.getRole())) { return true; }
        }
        return false;
    }

    private static boolean processForMediumPriority(Ticket ticket, List<Approver> approvers) {
        for (Approver a : approvers) {
            if ("担当者".equals(a.getRole())) continue;
            if ("リーダー".equals(a.getRole())) return true;
        }
        return false;
    }

    private static boolean processForHighPriority(Ticket ticket, List<Approver> approvers) {
        for (Approver a : approvers) {
            if ("担当者".equals(a.getRole())) continue;
            if ("リーダー".equals(a.getRole())) continue;
            if ("マネージャー".equals(a.getRole())) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        List<Approver> approvers = new ArrayList<>();
        approvers.add(new Approver("担当者"));
        approvers.add(new Approver("リーダー"));
        approvers.add(new Approver("マネージャー"));

        Ticket ticket1 = new Ticket("TKT-001", 0);
        Ticket ticket2 = new Ticket("TKT-002", 1);
        Ticket ticket3 = new Ticket("TKT-003", 2);

        System.out.println(processApproval(ticket1, approvers)); // true
        System.out.println(processApproval(ticket2, approvers)); // true
        System.out.println(processApproval(ticket3, approvers)); // true
    }
}

問題点は明らかです。優先度が増えるたびにprocessApproval()メソッドを修正する必要があり、既存ロジックへの影響が心配でテストケースも増えます。新しい承認者(「シニアエンジニア」等)を追加するには、各メソッドの中で条件文を書き足さなければならず、変更のたびにレビューとデプロイが必要です。

After: Chain of Responsibilityによる連鎖化

以下がリファクタリング後のコードです。各承認者はApproverChainとして振る舞い、次の承認者へ委譲します。

import java.util.ArrayList;
import java.util.List;

public class Ticket {
    private String id;
    private int priority;
    private boolean approvedByManager;

    public Ticket(String id, int priority) {
        this.id = id;
        this.priority = priority;
        this.approvedByManager = false;
    }

    public String getId() { return id; }
    public int getPriority() { return priority; }
    public boolean isApprovedByManager() { return approvedByManager; }
}

// 承認者インターフェース
public interface ApproverChain {
    void setNext(ApproverChain next);
    boolean approve(Ticket ticket);
}

// 抽象承認者(委譲チェーンの基本構造)
public abstract class AbstractApprover implements ApproverChain {
    protected String role;
    protected ApproverChain nextApprover;

    public AbstractApprover(String role) { this.role = role; }
    public String getRole() { return role; }

    @Override
    public void setNext(ApproverChain next) { this.nextApprover = next; }

    @Override
    public boolean approve(Ticket ticket) {
        if (canHandle(ticket)) {
            return handleApproval(ticket);
        } else if (nextApprover != null) {
            return nextApprover.approve(ticket);
        }
        return false;
    }

    protected abstract boolean canHandle(Ticket ticket);
    protected abstract boolean handleApproval(Ticket ticket);
}

// 担当者承認者
public class StaffApprover extends AbstractApprover {
    public StaffApprover() { super("担当者"); }

    @Override
    protected boolean canHandle(Ticket ticket) { return true; } // 全て担当者が受ける

    @Override
    protected boolean handleApproval(Ticket ticket) {
        System.out.println("[担当者] チケット " + ticket.getId() + " を承認しました。");
        if (ticket.getPriority() >= 1 && nextApprover != null) {
            return nextApprover.approve(ticket); // リーダーへ委譲
        }
        return true;
    }
}

// リーダー承認者
public class LeaderApprover extends AbstractApprover {
    public LeaderApprover() { super("リーダー"); }

    @Override
    protected boolean canHandle(Ticket ticket) { return ticket.getPriority() >= 1; }

    @Override
    protected boolean handleApproval(Ticket ticket) {
        System.out.println("[リーダー] チケット " + ticket.getId() + " を承認しました。");
        if (ticket.getPriority() >= 2 && nextApprover != null) {
            return nextApprover.approve(ticket); // マネージャーへ委譲
        }
        return true;
    }
}

// マネージャー承認者
public class ManagerApprover extends AbstractApprover {
    public ManagerApprover() { super("マネージャー"); }

    @Override
    protected boolean canHandle(Ticket ticket) { return ticket.getPriority() >= 2; }

    @Override
    protected boolean handleApproval(Ticket ticket) {
        System.out.println("[マネージャー] チケット " + ticket.getId() + " を承認しました。");
        return true;
    }
}

// シニアエンジニア承認者(新設:既存コードへの修正ゼロ)
public class SeniorApprover extends AbstractApprover {
    public SeniorApprover() { super("シニアエンジニア"); }

    @Override
    protected boolean canHandle(Ticket ticket) { return ticket.getPriority() >= 3; }

    @Override
    protected boolean handleApproval(Ticket ticket) {
        System.out.println("[シニアエンジニア] チケット " + ticket.getId() + " を承認しました。");
        if (nextApprover != null) {
            return nextApprover.approve(ticket); // 必要に応じてさらに委譲
        }
        return true;
    }
}

public class AfterMain {
    public static void main(String[] args) {
        // チェーンを構築:担当者→リーダー→マネージャー→シニアエンジニア
        ApproverChain staff = new StaffApprover();
        ApproverChain leader = new LeaderApprover();
        ApproverChain manager = new ManagerApprover();
        ApproverChain senior = new SeniorApprover();

        staff.setNext(leader);
        leader.setNext(manager);
        manager.setNext(senior);

        // 優先度0(担当者のみ)
        Ticket lowPriority = new Ticket("TKT-001", 0);
        System.out.println("--- TKT-001 (優先度: 0) ---");
        staff.approve(lowPriority);

        // 優先度1(リーダーまで)
        Ticket mediumPriority = new Ticket("TKT-002", 1);
        System.out.println("--- TKT-002 (優先度: 1) ---");
        leader.approve(mediumPriority);

        // 優先度2(マネージャーまで)
        Ticket highPriority = new Ticket("TKT-003", 2);
        System.out.println("--- TKT-003 (優先度: 2) ---");
        manager.approve(highPriority);

        // 優先度3(シニアエンジニアまで)— 既存コードへの影響なしで追加可能
        Ticket criticalPriority = new Ticket("TKT-004", 3);
        System.out.println("--- TKT-004 (優先度: 3) ---");
        senior.approve(criticalPriority);
    }
}

実行結果は以下の通りです。

--- TKT-001 (優先度: 0) ---
[担当者] チケット TKT-001 を承認しました。
--- TKT-002 (優先度: 1) ---
[リーダー] チケット TKT-002 を承認しました。
--- TKT-003 (優先度: 2) ---
[マネージャー] チケット TKT-003 を承認しました。
--- TKT-004 (優先度: 3) ---
[シニアエンジニア] チケット TKT-004 を承認しました。

新しい承認者を追加する際(例:「シニアエンジニア」)、既存コードを一切変更せずにSeniorApproverクラスを追加しただけで対応できました。各承認者がcanHandle()で自らの担当範囲を宣言するため、if-elseによる優先度分岐が不要になり、setNext()によるチェーン構築は承認フローの変更にも柔軟に対応できます。

🧓 ベテランの現場コラム 以前勤めていた現場で、顧客ごとにIF文を追加し続ける「承認ワークフロー」に直面したことがありました。結局300行超のエンドレスメソッドになり、テストも手作業で全パターン実行する始末でした。このパターンを知ってから、「委譲の連鎖」という発想ひとつで構造が劇的に整理されることを体感しました。

こんな場面で使う/使わない

  • 使う: 複数の処理者が存在し、リクエストが順次委譲されていく必要がある場合(例:エラーハンドリング、ログ出力チェーン)。処理者が増減しやすい環境で、既存コードへの影響を最小限に抑えたい場合
  • 使わない: 処理者が1つだけの場合(単一オブジェクトのメソッド呼び出しで十分)。全承認者が必ず通る必要がある場合(責任連鎖ではなく、単純な直列呼び出しの方が簡潔)

まとめ

  • Chain of Responsibilityは、リクエスト処理者を「順次委譲する連鎖」として抽象化し、if-elseによる分岐を排除するパターン
  • TicketCraftの承認フローでは、AbstractApproverを継承した各クラスがsetNext()でチェーン構築し、拡張性を確保
  • 既存コードへの影響を抑えつつ新しい承認者を追加できるため、保守性が向上する

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。