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

Iteratorパターン|チケット一覧の並び替えを呼び出し側に漏らさない

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

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

TicketCraftでは、チケット一覧を画面に表示する際に「最新順」「優先度順」などの巡回順序を切り替える機能が必要でした。しかし当時の実装では、表示順ごとに異なるメソッドが定義されており、新しい順序を追加する場合も、呼び出し側のコードごと修正する必要があり、保守性が低下していました。

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

Iteratorパターンは、コレクション内部の構造を隠蔽し、要素へのアクセス方法だけを統一して定義するデザインパターンです。レストランで店員が「お客様に料理を提供する」という役割だけを見ていればよいように、呼び出し側は内部構造の詳細を知る必要なく要素を巡回できるようになります。

Before

import java.util.*;

public class BeforeMain {
    static class Ticket {
        String id, status;
        int priority;
        Date createdDate;
        public Ticket(String id, String status, int priority) {
            this.id = id;
            this.status = status;
            this.priority = priority;
            this.createdDate = new Date();
        }
        @Override public String toString() { return "Ticket(" + id + ")"; }
    }

    static class TicketList {
        private final List<Ticket> tickets = Arrays.asList(
            new Ticket("T-1001", "OPEN", 3),
            new Ticket("T-1002", "IN_PROGRESS", 1),
            new Ticket("T-1003", "RESOLVED", 5)
        );

        public List<Ticket> getTicketsInOrderNew() {
            return tickets;
        }

        public List<Ticket> getTicketsInPriority() {
            List<Ticket> sorted = new ArrayList<>(tickets);
            sorted.sort(Comparator.comparingInt(t -> t.priority));
            return sorted;
        }
    }

    static void displayNew(TicketList list) {
        System.out.println("=== 最新順 ===");
        for (Ticket t : list.getTicketsInOrderNew()) {
            System.out.println("  " + t);
        }
    }

    static void displayPriority(TicketList list) {
        System.out.println("=== 優先度順 ===");
        for (Ticket t : list.getTicketsInPriority()) {
            System.out.println("  " + t);
        }
    }

    public static void main(String[] args) {
        TicketList tl = new TicketList();
        displayNew(tl);
        displayPriority(tl);
    }
}

このコードには問題があります。順序ごとに異なるメソッドが用意されており、呼び出し側は「どのメソッドを呼べばよいか」を知っている必要があるため、新しい表示順を追加するたびに呼び出し側の修正が必要になります。TicketListの内部実装がListであることも呼び出し側に露出しており、将来コレクションの実装を変更する場合、すべての呼び出し側で修正が発生してしまうリスクがあります。

After

import java.util.*;

public class AfterMain {
    static class Ticket {
        String id, status;
        int priority;
        Date createdDate;
        public Ticket(String id, String status, int priority) {
            this.id = id;
            this.status = status;
            this.priority = priority;
            this.createdDate = new Date();
        }
        @Override public String toString() { return "Ticket(" + id + ")"; }
    }

    interface TicketIterator extends Iterator<Ticket> {}

    static class NewestFirstIterator implements TicketIterator {
        private final List<Ticket> tickets;
        private int index = 0;
        public NewestFirstIterator(List<Ticket> tickets) { this.tickets = tickets; }
        @Override public boolean hasNext() { return index < tickets.size(); }
        @Override public Ticket next() { return tickets.get(index++); }
    }

    static class PriorityOrderIterator implements TicketIterator {
        private final List<Ticket> sortedTickets;
        private int index = 0;
        public PriorityOrderIterator(List<Ticket> tickets) {
            this.sortedTickets = new ArrayList<>(tickets);
            this.sortedTickets.sort(Comparator.comparingInt(t -> t.priority));
        }
        @Override public boolean hasNext() { return index < sortedTickets.size(); }
        @Override public Ticket next() { return sortedTickets.get(index++); }
    }

    static class TicketList {
        private final List<Ticket> tickets = Arrays.asList(
            new Ticket("T-1001", "OPEN", 3),
            new Ticket("T-1002", "IN_PROGRESS", 1),
            new Ticket("T-1003", "RESOLVED", 5)
        );

        public TicketIterator iteratorNew() {
            return new NewestFirstIterator(tickets);
        }

        public TicketIterator iteratorPriority() {
            return new PriorityOrderIterator(tickets);
        }
    }

    static void display(TicketList list, String title, TicketIterator it) {
        System.out.println("=== " + title + " ===");
        while (it.hasNext()) {
            System.out.println("  " + it.next());
        }
    }

    public static void main(String[] args) {
        TicketList tl = new TicketList();
        display(tl, "最新順", tl.iteratorNew());
        display(tl, "優先度順", tl.iteratorPriority());
    }
}

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

=== 最新順 ===
  Ticket(T-1001)
  Ticket(T-1002)
  Ticket(T-1003)
=== 優先度順 ===
  Ticket(T-1002)
  Ticket(T-1001)
  Ticket(T-1003)

TicketListの内部構造は呼び出し側から隠蔽され、表示順序だけをイテレータの実装として切り替えられるようになりました。新しい順序を追加する場合も、新たにTicketIteratorを実装したクラスを定義し、TicketListにメソッドを追加するだけで済み、呼び出し側のdisplayメソッドには一切修正が不要です。

🧓 ベテランの現場コラム 一度コレクションの巡回方法をイテレータとして切り出してしまえば、その後の順序追加は実装クラスを1つ足すだけで完結します。「単純なfor文で十分」と思っていた処理ほど、後から並び替えの要望が増えていくものです。

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

  • 使う: コレクションの要素に同じ操作を行いたいが、内部構造や巡回順序が変わる可能性がある場合
  • 使わない: 単純なforeachで事足りる場合や、Stream APIのソート機能で十分解決できる場合(導入コストに見合わないことが多い)

まとめ

  • Iteratorパターンはコレクションの内部構造を隠蔽し、要素へのアクセス方法だけを統一する
  • TicketCraftのように表示順序を切り替えたい場合、イテレータの実装を変えるだけで呼び出し側コードを変更せずに済む
  • 単純な巡回にStream APIのソートが使えるなら、あえてこのパターンを導入する必要はない

🎓 理解度テスト

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