Iterator パターン
コレクションの内部構造を公開せずに、要素を順番にアクセスする手段を提供するデザインパターン
Iterator パターンとは
Iterator パターンは、コレクション (配列、ツリー、グラフ) の内部構造を公開せずに、要素を 1 つずつ順番にアクセスする統一的なインターフェースを提供するデザインパターンである。GoF の 23 パターンの 1 つで、現代の言語では言語仕様に組み込まれている。
配列は for ループで簡単に反復できるが、ツリー構造やページネーション付きの API レスポンスはそうはいかない。Iterator パターンは、データ構造の違いを隠蔽し、統一的な for...of で反復可能にする。
JavaScript の Iterator プロトコル
JavaScript には言語レベルで Iterator が組み込まれている。Symbol.iterator メソッドを実装したオブジェクトは Iterable と呼ばれ、for...of、スプレッド構文、分割代入で使える。
class Range {
constructor(private start: number, private end: number) {}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next(): IteratorResult<number> {
if (current <= end) return { value: current++, done: false };
return { value: undefined, done: true };
},
};
}
}
for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4, 5
console.log([...new Range(1, 3)]); // [1, 2, 3]
const [first, second] = new Range(10, 20); // 10, 11
next() メソッドが { value, done } を返すのが Iterator プロトコルの核心だ。done: true になるまで next() を呼び続ける。
Generator 関数
Generator 関数 (function*) は Iterator を簡潔に作成する構文糖だ。yield で値を 1 つずつ返し、関数の実行が一時停止・再開される。
function* fibonacci(): Generator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 無限シーケンスから最初の 10 個だけ取得
function take<T>(n: number, iter: Iterable<T>): T[] {
const result: T[] = [];
for (const value of iter) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
take(10, fibonacci()); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Generator は無限シーケンスを表現できる。全要素をメモリに載せる必要がなく、必要な分だけ生成する遅延評価が自然に実現される。
Async Iterator
for await...of と Symbol.asyncIterator で、非同期データソースを反復できる。DynamoDB のページネーションや、ストリーミング API の処理に使う。
async function* paginateQuery(params: QueryCommandInput) {
let lastKey: Record<string, any> | undefined;
do {
const result = await ddb.send(new QueryCommand({
...params,
ExclusiveStartKey: lastKey,
}));
yield* result.Items ?? [];
lastKey = result.LastEvaluatedKey;
} while (lastKey);
}
// 全ページを透過的に反復
for await (const item of paginateQuery({ TableName: 'Orders', /* ... */ })) {
console.log(item);
}
AWS SDK v3 の paginateQuery もこのパターンで実装されている。呼び出し側はページネーションの存在を意識せず、for await...of で全アイテムを処理できる。
遅延評価のメリット
Iterator の最大の利点は遅延評価だ。100 万件のデータを処理する場合、配列なら全件をメモリに載せる必要があるが、Iterator なら 1 件ずつ処理できる。
// ❌ 全件をメモリに載せる
const allItems = await fetchAllItems(); // 100万件 → メモリ不足
allItems.filter(item => item.active).map(item => transform(item));
// ✅ Iterator で 1 件ずつ処理
for await (const item of fetchItemsIterator()) {
if (item.active) await processItem(transform(item));
}
Iterator パターンの適用場面
| 場面 | 具体例 |
|---|---|
| ページネーション | DynamoDB の Query、REST API のページ送り |
| ファイル処理 | 大きなファイルを 1 行ずつ読む |
| ストリーミング | WebSocket メッセージ、SSE |
| ツリー走査 | DOM ツリー、ファイルシステムの再帰探索 |
| 無限シーケンス | フィボナッチ数列、乱数生成 |
全体像を把握するには関連書籍も有用。