イテレータパターンとは
イテレータパターン(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文の方がコード量が少なく済み、イテレータクラスを新たに定義するのは冗長になる可能性があります。