Javaのデザインパターンを学習していて、Iteratorパターンで少しつまずきました。
教材として読んでいるのは『Java言語で学ぶデザインパターン入門 第3版』です。
Iteratorパターンについて、
- Iterator
- Aggregate
- ConcreteIterator
- ConcreteAggregate
という4つの役割が登場することは分かりました。
しかし、最初はそれぞれが何のために存在するのか、特に
なぜConcreteAggregateがConcreteIteratorを生成するのか
がなかなか腑に落ちませんでした。
この記事では、Iteratorパターンを学習する中で自分がつまずいたポイントと、そこからどのように理解したかを整理します。
Iteratorパターンとは
Iteratorパターンは、複数の要素を持つオブジェクトから、要素を順番に取り出すためのデザインパターンです。
本棚を例にすると、
BookShelfが複数の本を持つBookShelfIteratorが本を1冊ずつ順番にたどる
という役割分担になります。
重要なのは、単に「繰り返し処理を共通化する」ということではありません。
学習を進める中で、Iteratorパターンのポイントは、
データをどう保持するかと、そのデータをどう走査するかを切り離すこと
だと理解しました。
最初につまずいたこと
最初は、Iteratorインターフェースがあることで、
「一つずつ繰り返す」という処理がコレクションから外に出されている
くらいの理解でした。
これは大きく間違ってはいませんでした。
ただ、「外に出すことで何がうれしいのか」が分かっていませんでした。
そこで本棚の例で考えてみました。
BookShelfが直接走査される場合
例えば、BookShelf が内部で配列を持っているとします。
class BookShelf {
private Book[] books;
}
利用する側がこの配列を直接取得して走査するとします。
Book[] books = bookShelf.getBooks();
for (int i = 0; i < books.length; i++) {
System.out.println(books[i].getName());
}
このコードには一つ問題があります。
利用する側が、
BookShelfの内部ではBook[]が使われている
ということを知ってしまっています。
つまり、利用側がBookShelfの内部構造に依存しています。
配列からListに変更したらどうなるか
ここで、BookShelf内部のデータ構造を変更するとします。
private Book[] books;
から、
private List<Book> books;
に変更します。
すると、
Book[] books = bookShelf.getBooks();
というコードはそのままでは使えません。
BookShelf内部の変更なのに、BookShelfを利用しているコードまで修正する必要が出てきます。
これを避けるためにIteratorを利用します。
Iteratorを使う
利用側では、BookShelf内部のデータ構造を直接触らずにIteratorを取得します。
Iterator<Book> iterator = bookShelf.iterator();
while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println(book.getName());
}
利用側が知っているのは、
hasNext()
next()
というIteratorの操作だけです。
BookShelf内部が、
Book[]
なのか、
ArrayList<Book>
なのか、
LinkedList<Book>
なのかを知る必要はありません。
このとき、
BookShelfのデータの持ち方
と、
BookShelfの中身をどうたどるか
が分離されています。
IteratorとAggregateの役割
ここで、Iteratorパターンに登場する4つの役割を整理します。
Iterator
Iteratorは、要素を順番にたどるための操作を定義します。
例えば次のようなインターフェースです。
public interface Iterator<E> {
boolean hasNext();
E next();
}
ここでは、
- 次の要素が存在するか
- 次の要素を取得する
という操作だけを定義しています。
実際にどのように要素を取得するかは実装していません。
ConcreteIterator
Iteratorを具体的に実装するクラスです。
本棚の場合は BookShelfIterator です。
public class BookShelfIterator implements Iterator<Book> {
private BookShelf bookShelf;
private int index;
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}
@Override
public Book next() {
Book book = bookShelf.getBookAt(index);
index++;
return book;
}
}
このクラスが、
BookShelfをどういう順番でたどるか
を知っています。
Aggregate
Aggregateは、Iteratorを取得するためのインターフェースです。
public interface Aggregate<E> {
Iterator<E> iterator();
}
最初はこのインターフェースの存在理由がよく分かりませんでした。
しかし、
「私は、自分を走査するためのIteratorを渡すことができます」
という約束を表していると考えると理解しやすくなりました。
ConcreteAggregate
Aggregateを実装した具体的なコレクションです。
今回の場合は BookShelf です。
public class BookShelf implements Aggregate<Book> {
private List<Book> books;
@Override
public Iterator<Book> iterator() {
return new BookShelfIterator(this);
}
}
ここで一番疑問だったのが、
return new BookShelfIterator(this);
という部分でした。
なぜBookShelfがBookShelfIteratorを生成するのか
最初は、
「走査するのはIteratorなのに、なぜBookShelf側でIteratorを作るのか?」
と思いました。
しかし、利用側から考えると理由が分かりました。
もしBookShelfがIteratorを生成しなければ、利用側は次のように書く必要があります。
BookShelf bookShelf = new BookShelf();
BookShelfIterator iterator =
new BookShelfIterator(bookShelf);
このコードを書くためには、
BookShelfを走査するにはBookShelfIteratorを使う
という具体的なクラスの組み合わせを利用側が知っている必要があります。
一方、BookShelf自身がIteratorを生成すれば、
Iterator<Book> iterator = bookShelf.iterator();
だけで済みます。
意味としては、
「この本棚を走査するためのIteratorをください」
とBookShelfに頼んでいるイメージです。
BookShelfは、
「自分を走査するならBookShelfIteratorを使えばよい」
ということを知っているので、自分自身を渡してIteratorを生成します。
new BookShelfIterator(this)
利用側は BookShelfIterator という具体クラスすら意識する必要がありません。
<E> は何を意味しているのか
IteratorやAggregateには次のような記述があります。
Iterator<E>
この <E> はJavaのジェネリクスです。
E は一般的にElement(要素)を意味します。
例えば、
Iterator<Book>
なら、
E = Book
です。
そのため、
E next();
は、Iterator<Book> として使用すると、
Book next();
として扱われます。
Aggregateも同じです。
public interface Aggregate<E> {
Iterator<E> iterator();
}
例えば、
class BookShelf implements Aggregate<Book>
なら、
Iterator<Book> iterator();
という意味になります。
つまり、
Iterator<E> iterator();
は、
「
iterator()を実行すると、E型の要素を扱うIteratorが返される」
という意味です。
最初は、
「E型のIteratorのiteratorメソッドを実行する」
という意味なのかと思っていましたが、そうではありませんでした。
Javaのメソッド宣言として見ると、
Iterator<E> iterator();
は、
Iterator<E> → 戻り値
iterator → メソッド名
() → 引数なし
と分解できます。
Iteratorパターンのクラス関係
今回理解した関係を整理すると、次のようになります。
<<interface>>
Aggregate<E>
↑
│ implements
│
BookShelf
│
│ creates
▼
BookShelfIterator
│
│ implements
▼
<<interface>>
Iterator<E>
それぞれの役割を一言で表すと、
Aggregate
「Iteratorを取得できます」
ConcreteAggregate
「実際のデータを持ち、
自分を走査するIteratorを作ります」
Iterator
「どのような操作で走査するかを定義します」
ConcreteIterator
「実際の走査方法を実装します」
となります。
Iteratorパターンのメリット
今回の学習で一番理解が変わったのがここでした。
最初は、
Iteratorを使えば繰り返し処理を外に出せる
という理解でした。
現在は、
コレクションの内部構造と、それを利用するコードの依存を弱くできる
ことが大きなメリットだと理解しています。
例えばBookShelf内部を、
Book[]
↓
ArrayList<Book>
に変更したとします。
Iteratorを使っていなければ、その変更が利用側まで波及する可能性があります。
しかし、利用側が、
while (iterator.hasNext()) {
Book book = iterator.next();
}
というIteratorのインターフェースだけに依存していれば、BookShelf内部の変更を意識する必要がありません。
つまり、
内部構造を隠すこと自体が目的なのではなく、内部構造を隠すことで変更の影響範囲を小さくする
ということです。
これはIteratorパターンだけでなく、オブジェクト指向全体にもつながる考え方だと思いました。
学習前後で理解がどう変わったか
学習前は、
Iteratorは繰り返し処理をするためのクラス
くらいに考えていました。
しかし今回整理したことで、
データを保持する責務と、そのデータを走査する責務を分離する
という設計上の意味が見えるようになりました。
さらに、
bookShelf.iterator();
についても、
BookShelfに「自分を走査するためのIteratorをください」と依頼している
と考えることで、ConcreteAggregateがConcreteIteratorを生成する理由も理解しやすくなりました。
Iteratorパターンの本質は、単にfor文を別クラスに移すことではありません。
データ構造の実装詳細を利用側から隠し、共通のIteratorというインターフェースを通して走査できるようにすることで、変更への依存を減らすパターン
だと、現時点では理解しています。
次に確認したいこと
今回で基本的な役割は整理できました。
次は教材のBookShelfをそのまま書き写すのではなく、例えば、
ShoppingCart
Product
ProductIterator
のように別の題材で、
- Aggregate
- ConcreteAggregate
- Iterator
- ConcreteIterator
を自分で設計してみたいと思います。
教材を見ずにクラス図とコードを再現できれば、Iteratorパターンを「読んで分かった」状態から「自分で使える」状態に一歩近づけそうです。