※この投稿はJava言語で学ぶデザインパターン入門を読んだ感想と復習を兼ねたまとめです
Iteratorパターンは、デザインパターンの1つで、集合に含まれる要素を1つずつ取り出したいときに使用されるようです。
次のようなクラス図で説明されていました。

Aggregateは集合という意味で、配列などの要素の集まりを指すようです。コンテナオブジェクトとも呼ばれるようでした。
Iteratorは反復子と訳され、Iterateする(繰り返す)ものを意味します。これは実装例を見たほうがわかりやすかったです。
$books = $bookShelf->getBooks();
for ($i = 0; $i < count($books); $i++) {
echo $books[$i]->getName() . "\n";
}
上記はIteratorパターンを使用せず、for文を利用して各要素を取得した例です。
本棚(BooKShelf)からすべての本(Books)を取り出し、先頭から順番に本のタイトルを出力しています。
これをIteratorパターンで置き換えると次のようになります。
$iterator = $bookShelf->iterator();
while ($iterator->hasNext()) {
echo $iterator->next()->getName() . "\n";
}
本棚はiterator()でIteratorインスタンスを生成しています。
反復子は、hasNext()で次の要素があるかどうかを調べ、next()で次の要素を取得しています。
また、関数名からはわかりづらいですが、next()で反復子の現在地を次に進めています。
class BookShelfIterator implements IteratorInterface
{
private $bookShelf;
private $index;
public function __construct(BookShelf $bookShelf)
{
$this->bookShelf = $bookShelf;
$this->index = 0;
}
public function hasNext(): bool
{
return $this->index < $this->bookShelf->getLength();
}
public function next(): object
{
$book = $this->bookShelf->getBookAt($this->index);
$this->index++;
return $book;
}
}
Iteratorパターンは、このように集合の要素を1つずつ取り出して処理したいときに使われるようです。
集合の内部仕様に依存しない反復処理が書ける
Iteratorパターンを利用する最大の利点は、集合の要素を列挙する手段を独立させることで、集合の内部仕様に依存しないIteratorを提供することにあるようです。
集合の内部仕様ときいて、最初はどういう意味かわかりませんでしたが、たとえば集合が配列(添字が0から始まる数字)から連想配列(キーとして文字列などを使う)に変更になった場合を想像するとわかりやすかったです。
集合の内部仕様が、配列から連想配列に変わると、さきほど挙げた次のfor文を修正しなければなりません。
// $booksが連想配列の場合は修正しなければならない
$books = $bookShelf->getBooks();
for ($i = 0; $i < count($books); $i++) {
echo $books[$i]->getName() . "\n";
}
しかしIteratorパターンで実装していれば、呼び出し元のロジックは変更しなくてもよくなります。
// $booksが連想配列になってもIteratorだけ修正すればよい
$iterator = $bookShelf->iterator();
while ($iterator->hasNext()) {
echo $iterator->next()->getName() . "\n";
}
このように、集合の内部仕様に依存せず、各要素を列挙できるようになることがIteratorパターンの最大の利点のようです。
呼び出し元に修正を加えずに列挙方法を変更できる
さきほどは集合の内部仕様が変更になる例でしたが、集合はそのままだけれど、要素の列挙方法を変更したい場合にもIteratorパターンは有効です。
たとえば、集合の要素を先頭から順方向に列挙するのではなく、後方から前に進めたり、その他の複雑な順序で列挙したい場合などです。要素の列挙方法を定義しているのはIteratorなので、Iteratorを修正すれば、呼び出し元に手を加えずに列挙順序を変更することが可能です。
// 逆方向に要素を列挙するように変更した例
class BookShelfIterator implements IteratorInterface
{
private $bookShelf;
private $index;
public function __construct(BookShelf $bookShelf)
{
$this->bookShelf = $bookShelf;
$this->index = $this->bookShelf->getLength() - 1; // 最後の要素から開始する
}
public function hasNext(): bool
{
return $this->index > 0; // indexが0以上であれば次の要素が存在する
}
public function next(): object
{
$book = $this->bookShelf->getBookAt($this->index);
$this->index--; // 要素を列挙した後はindexを1減らす
return $book;
}
}
このようにIteratorクラスの実装を変更するだけで、要素の列挙方法を変更することができます。
上記の例では既存のIteratorを修正していますが、列挙方法の異なるIteratorを複数作ることも可能です。インタフェースは共通しているので、呼び出し元で別のIteratorに切り替えることも簡単にできます。複数の箇所で、複数のパターンで集合要素が列挙されるような場合には、特にIteratorパターンは有効に思えました。
Iteratorパターンのメリット
Iteratorパターンは、集合の内部仕様と要素の列挙手段を独立させることを目的としていました。
それにより、次のようなメリットが得られます。
- 集合の内部仕様が変更されても、呼び出し元の処理を修正する必要がなくなる
- 要素の列挙方法を変更したい場合も、呼び出し元の処理を修正する必要がない
- 集合に対して複数の列挙方法を提供したい場合も、簡単に切り替えることができる
最後に、Iteratorパターンの実装例(PHP)を載せておきます。
<?php
interface AggregateInterface
{
public function iterator(): IteratorInterface;
}
interface IteratorInterface
{
public function hasNext(): bool;
public function next(): object;
}
class Book
{
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class BookShelf implements AggregateInterface
{
private $books;
public function getLength(): int
{
return count($this->books);
}
public function appendBook(Book $book): void
{
$this->books[] = $book;
}
public function getBooks(): array
{
return $this->books;
}
public function getBookAt(int $index): Book
{
return $this->books[$index];
}
public function iterator(): IteratorInterface
{
return new BookShelfIterator($this);
}
}
class BookShelfIterator implements IteratorInterface
{
private $bookShelf;
private $index;
public function __construct(BookShelf $bookShelf)
{
$this->bookShelf = $bookShelf;
$this->index = 0;
}
public function hasNext(): bool
{
return $this->index < $this->bookShelf->getLength();
}
public function next(): object
{
$book = $this->bookShelf->getBookAt($this->index);
$this->index++;
return $book;
}
}
function main()
{
$bookShelf = new BookShelf();
$bookShelf->appendBook(new Book('book-A'));
$bookShelf->appendBook(new Book('book-B'));
$bookShelf->appendBook(new Book('book-C'));
$bookShelf->appendBook(new Book('book-D'));
$iterator = $bookShelf->iterator();
while ($iterator->hasNext()) {
echo $iterator->next()->getName() . "\n";
}
$books = $bookShelf->getBooks();
for ($i = 0; $i < count($books); $i++) {
echo $books[$i]->getName() . "\n";
}
}
main();