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

Strategyパターン|チケット割り当てロジックを実行時に切り替える

  • Java
  • デザインパターン
  • Strategy
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before
  3. After
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftでは、新しく作成されたチケットを誰に割り当てるかを決定するロジックが単一のクラスに固められていました。開発者はラウンドロビン方式で割り当てていましたが、ある日チームから「負荷の少ない人に優先的に回したい」という要望が上がりました。既存コードを修正すると、今度は別のチームから別の割り当てルールの要望が届き、条件分岐が深くなるばかりでした。

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

Strategyパターンは、アルゴリズムの実行時切り替えを実現するためのデザインパターンです。レストランで、予約時に「窓側席」「カウンター席」「個室」を注文者の状況に応じて動的に選択できるようなイメージです。

Before

public class BeforeMain {
    public static void main(String[] args) {
        AssignmentManager manager = new AssignmentManager();
        Ticket ticket1 = new Ticket(1, "ログインエラー");
        Ticket ticket2 = new Ticket(2, "パスワードリセット");
        Ticket ticket3 = new Ticket(3, "API更新情報確認");

        User alice   = new User("Alice", 0);
        User bob     = new User("Bob", 1);
        User charlie = new User("Charlie", 2);

        manager.assignTicket(ticket1, alice, bob, charlie);
        System.out.println(ticket1.getAssignee().getName());

        manager.assignTicket(ticket2, alice, bob, charlie);
        System.out.println(ticket2.getAssignee().getName());

        manager.assignTicket(ticket3, alice, bob, charlie);
        System.out.println(ticket3.getAssignee().getName());
    }
}

class AssignmentManager {
    public void assignTicket(Ticket ticket, User... users) {
        // ラウンドロビン方式で固定
        int index = (int)(ticket.getId() % users.length);
        users[index].addPendingWork();
        ticket.assign(users[index]);
    }
}

class Ticket {
    private final int id;
    private final String title;
    private boolean assigned;
    private User assignee;

    public Ticket(int id, String title) { this.id = id; this.title = title; this.assigned = false; }

    public void assign(User user) { this.assignee = user; this.assigned = true; }
    public int getId() { return id; }
    public boolean isAssigned() { return assigned; }
    public User getAssignee() { return assignee; }
}

class User {
    private final String name;
    private final int userId;
    private int pendingWork;

    public User(String name, int userId) { this.name = name; this.userId = userId; }
    public void addPendingWork() { pendingWork++; }
    public String getName() { return name; }
}

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

Bob
Charlie
Alice

このコードの問題は、assignTicketメソッドの中に割り当てルールがハードコーディングされている点です。ラウンドロビン方式から負荷分散方式へ切り替えるにはメソッド全体を書き換える必要があり、複数のルールを共存させるには条件分岐を追加しなければならなくなります。

After

public class AfterMain {
    public static void main(String[] args) {
        User alice   = new User("Alice", 0);
        User bob     = new User("Bob", 1);
        User charlie = new User("Charlie", 2);

        Ticket ticket1 = new Ticket(1, "ログインエラー");
        Ticket ticket2 = new Ticket(2, "パスワードリセット");
        Ticket ticket3 = new Ticket(3, "API更新情報確認");

        AssignmentManager manager = new AssignmentManager(new RoundRobinAssignment());
        manager.assignTicket(ticket1, alice, bob, charlie);
        System.out.println(ticket1.getAssignee().getName() + ": ラウンドロビン方式");

        manager.setStrategy(new LoadBalancedAssignment());
        manager.assignTicket(ticket2, alice, bob, charlie);
        System.out.println(ticket2.getAssignee().getName() + ": 負荷分散方式");

        manager.setStrategy(new RoundRobinAssignment());
        manager.assignTicket(ticket3, alice, bob, charlie);
        System.out.println(ticket3.getAssignee().getName() + ": ラウンドロビン方式");
    }
}

interface AssignmentStrategy {
    User assign(User... users);
}

class RoundRobinAssignment implements AssignmentStrategy {
    private int index = 0;

    public User assign(User... users) {
        User chosen = users[index % users.length];
        chosen.addPendingWork();
        index++;
        return chosen;
    }
}

class LoadBalancedAssignment implements AssignmentStrategy {
    public User assign(User... users) {
        User lightest = null;
        int minLoad = Integer.MAX_VALUE;
        for (User u : users) {
            if (u.getPendingWork() < minLoad) {
                minLoad = u.getPendingWork();
                lightest = u;
            }
        }
        lightest.addPendingWork();
        return lightest;
    }
}

class AssignmentManager {
    private AssignmentStrategy strategy;

    public AssignmentManager(AssignmentStrategy strategy) { this.strategy = strategy; }
    public void setStrategy(AssignmentStrategy strategy) { this.strategy = strategy; }

    public void assignTicket(Ticket ticket, User... users) {
        User chosen = strategy.assign(users);
        ticket.assign(chosen);
    }
}

class Ticket {
    private final int id;
    private final String title;
    private boolean assigned;
    private User assignee;

    public Ticket(int id, String title) { this.id = id; this.title = title; }
    public void assign(User user) { this.assignee = user; this.assigned = true; }
    public int getId() { return id; }
    public boolean isAssigned() { return assigned; }
    public User getAssignee() { return assignee; }
}

class User {
    private final String name;
    private final int userId;
    private int pendingWork;

    public User(String name, int userId) { this.name = name; this.userId = userId; }
    public void addPendingWork() { pendingWork++; }
    public String getName() { return name; }
    public int getPendingWork() { return pendingWork; }
}

実行結果は以下の通りです。1件目はラウンドロビンでAliceに割り当てた結果、Aliceの保留件数が1になっており、負荷分散方式に切り替えた2件目はまだ0件のBobに割り当てられます。

Alice: ラウンドロビン方式
Bob: 負荷分散方式
Alice: ラウンドロビン方式

AssignmentStrategyインターフェースを通じて割り当てルールを抽象化し、setStrategyメソッドで実行時に切り替えられるようになりました。新しいルールを追加しても既存コードを変更する必要がなく、新しいクラスを実装して設定するだけで済みます。

🧓 ベテランの現場コラム Strategyパターンを適用した直後は「ただインターフェースが増えただけ」に見えても、3つ目・4つ目の戦略が追加された瞬間にその価値が明確になります。最初の1つや2つだけなら、正直if文でも大差はありません。

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

  • 使う: ランタイムでアルゴリズムを切り替えたい場合。同じ目的に対して複数のアプローチが存在し、条件によって使い分けたい場合
  • 使わない: アルゴリズムが1つだけで変更の予定がない場合。単純なif文数行で十分カバーできる場合

まとめ

  • Strategyパターンは「実行時にアルゴリズムを切り替えたい」場面で力を発揮する
  • if-elseチェーンによる条件分散を防ぎ、新しいルールを追加しても既存コードに手を加えない設計が可能になる
  • 具体的な実装はStrategyインターフェースと、それを具現化する複数のクラスとして分離するのが一般的

🎓 理解度テスト

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