イテレータパターンの詳細と実装例

イテレータパターンとは

イテレータパターン(Iterator Pattern)は、コレクションオブジェクトの内部構造を公開することなく、その要素に順次アクセスするためのインターフェースを提供する振る舞いに関するデザインパターンです。このパターンは、集合体(Aggregate)の走査処理をイテレータ(Iterator)という別のオブジェクトに委譲することで、クライアントコードからデータ構造の詳細を隠蔽します(カプセル化)。

本質的な目的は、データ構造の複雑さ(リスト、木構造、ハッシュテーブルなど)を利用者から隔離し、一貫した方法で要素を扱えるようにすることです。これにより、配列であっても連結リストであっても、同じメソッド呼び出し(`next()`や`hasNext()`など)で要素を取り出すことが可能になります。

適用シーンと役割

現実世界の例として、工場のベルトコンベアや交通機関の自動改札機が挙げられます。ベルトコンベア上の荷物や改札を通る人々という「集合体」に対して、スキャナーやゲートという「統一されたインターフェース」を通じてアクセスします。中身が異なっていても、処理の流れは統一されています。

イテレータパターンは以下のシーンで特に有効です。

  • 集合体の内部表装(配列、リスト、ツリーなど)を公開せずに要素にアクセスしたい場合。
  • 異なるデータ構を持つ複数のコレクションに対して、統一された走査ロジックを適用したい場合。

クラス図と構成要素

イテレータパターンは主に以下の4つの役割で構成されます。

  • Iterator(イテレータ): 要素の順次アクセスを行うためのメソッド(次の要素取得、終了判定など)を定義するインターフェース。
  • ConcreteIterator(具象イテレータ): Iteratorインターフェースを実装し、特定の集合体の構造に合わせた走査ロジック(カーソルの管理など)を保持するクラス。
  • Aggregate(集合体): イテレータを作成するためのインターフェースを定義する役割。
  • ConcreteAggregate(具象集合体): 実際のデータ構造を管理し、対応するConcreteIteratorのインスタンスを生成して返すクラス。

実装例:基本的なイテレータ

Javaの標準ライブラリでは`java.util.Iterator`インターフェースが有名ですが、ここではパターンの理解を深めるために、カスタムの本棚(BookShelf)クラスとそのイテレータを実装します。変数名や構造を変更し、概念的な再現を行います。

// 1. 抽象イテレータの定義
public interface IIterator<T> {
    boolean hasNext();
    T next();
}

// 2. 具象集合体:本棚クラス
public class BookShelf {
    private String[] books;
    private int index = 0;

    public BookShelf(int maxSize) {
        this.books = new String[maxSize];
    }

    public void appendBook(String bookName) {
        if (index < books.length) {
            this.books[index] = bookName;
            index++;
        }
    }

    // イテレータを生成するメソッド
    public IIterator<String> createIterator() {
        return new BookShelfIterator(this);
    }

    public int getLength() {
        return index;
    }

    public String getBookAt(int index) {
        return books[index];
    }
}

// 3. 具象イテレータ:本棚の走査ロジック
public class BookShelfIterator implements IIterator<String> {
    private BookShelf shelf;
    private int currentPosition;

    public BookShelfIterator(BookShelf shelf) {
        this.shelf = shelf;
        this.currentPosition = 0;
    }

    @Override
    public boolean hasNext() {
        // 現在の位置が本棚のサイズより小さいか確認
        return currentPosition < shelf.getLength();
    }

    @Override
    public String next() {
        if (!hasNext()) {
            throw new IllegalStateException("これ以上要素がありません。");
        }
        String book = shelf.getBookAt(currentPosition);
        currentPosition++;
        return book;
    }
}

発展的な実装例:双方向および遅延ロード

標準的な`ArrayList`のようなリスト構造では、前後の要素への移動が必要になる場合があります。また、データベースの結果セット(MyBatisのCursorなど)のように、大量のデータを一度にメモリに載せず、必要に応じて取得する「遅延評価」を行うイテレータも存在します。

以下に、双方向の移動(前の要素へ戻る)をサポートしたイテレータの構造例を示します。

// 双方向移動を可能にする拡張イテレータ
public interface IBidirectionalIterator<T> extends IIterator<T> {
    boolean hasPrevious();
    T previous();
}

public class AdvancedList<T> {
    private Object[] dataStore;
    private int size;

    public AdvancedList(int capacity) {
        this.dataStore = new Object[capacity];
        this.size = 0;
    }

    public void add(T item) {
        if (size < dataStore.length) {
            dataStore[size++] = item;
        }
    }

    public IBidirectionalIterator<T> bidirectionalIterator() {
        return new ListIteratorImpl(this);
    }

    // 内部実装クラス
    private class ListIteratorImpl implements IBidirectionalIterator<T> {
        private int cursor = 0; // 次に返す要素のインデックス

        @Override
        public boolean hasNext() {
            return cursor < size;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T next() {
            if (!hasNext()) throw new NoSuchElementException();
            return (T) dataStore[cursor++];
        }

        @Override
        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        @Override
        public T previous() {
            if (!hasPrevious()) throw new NoSuchElementException();
            return (T) dataStore[--cursor];
        }
    }
}

また、データベースからのデータ取得のようなシナリオでは、以下のような遅延ロード型のイテレータが考えられます。これはMyBatisの`DefaultCursor`のような実装の論理を簡略化したものです。

public class LazyLoaderIterator implements IIterator<DataRow> {
    private DataSource dataSource;
    private DataRow cachedRow;
    private boolean isFetched = false;

    public LazyLoaderIterator(DataSource source) {
        this.dataSource = source;
    }

    @Override
    public boolean hasNext() {
        if (!isFetched) {
            cachedRow = dataSource.fetchNextRow(); // 実際のDBアクセスはここで発生
            isFetched = true;
        }
        return cachedRow != null;
    }

    @Override
    public DataRow next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        DataRow result = cachedRow;
        // フラグをリセットし、次回hasNext呼び出し時に次の行を取得させる
        isFetched = false;
        cachedRow = null;
        return result;
    }
}

イテレータパターンのメリットとデメリット

メリット:

  • 責任の分離(Single Responsibility): コレクションクラスは「データの管理」に、イテレータクラスは「走処理」に専念できるため、役割が明確になります。
  • カプセル化の維持: コレクション内部の実装(配列かリストかなど)を変更しても、イテレータのインターフェースが変わらなければ、クライアントコードに影響を与えません。
  • 多様な走処理の提供: 1つのコレクションに対して、順方向、逆方向、フィルタリング付きなど、複数種類のイテレータを提供することが容易になります。

デメリット:

  • 単純なケースでの複雑化: 単純な配列のループ処理程度であれば、標準的なfor文や拡張for文の方がコード量が少なく済み、イテレータクラスを新たに定義するのは冗長になる可能性があります。

タグ: Java Design Patterns Iterator Data Structures Refactoring

5月17日 16:09 投稿