Iteratorパターンの例
Iterator(反復)がデザインパターンだなんて冗談じゃない。オブジェクトのコレクションを順番に片付けるやり方なんて、プログラミングを覚え始めた頃から知ってるよ――そう思った人もいるのではないでしょうか。
ただ、考えてもみてください。Iteratorをさまざまなデザインパターンの1つとして正式に認めた書籍『デザインパターン』が刊行されたのは1995年のことなのです。Javaはまだ普及しておらず、C++が主なオブジェクト指向言語だった時代です。オブジェクト指向開発そのものは、ほとんどのプログラマにとってまだ目新しいものでした。
当時のC++コーダーは、コレクションに対して反復処理を行う仕組みを自作するのが一般的でした。例えば、prev
メソッドとnext
メソッドを書いて、リンクリスト内を前後に移動できるようにする、といった方法です。また、コレクションによっては、カプセル化されたデータ構造(配列など)を指すポインタを公開して、クライアント側でデータ構造を反復処理できるようにすることさえありました。多くの場合、反復処理を実現しようとすれば、基盤のコレクション実装について具体的な詳細情報を多量に公開する必要がありました。
同じ時期に流行していたSmalltalkには、さまざまな内部イテレータが用意されていました。これはクロージャの使用によって実現された機能です。内部イテレータはコレクションの反復処理を制御するのに対して、外部イテレータはクライアントが反復処理を制御することを可能にします。一般に、外部イテレータは柔軟性を高めるためのもので、コレクションクラスの作成者が意図しなかったような反復処理をクライアント側で定義することを可能にします。それとは対照的に、内部イテレータはクライアントコード側の作業を簡略化するものです。
Smalltalkプログラマはごく簡単に内部イテレータを利用できました。なんらかのコレクションを反復処理する必要が出てきた場合には、あらかじめ用意されている反復メカニズム(do:
、select:
、detect:
、reject:
、inject:into:
)をいつでも使用できます。
Javaでは、外部イテレータに対応するいくつかのコレクションクラスが用意されました。基底コレクションクラスであるVector
には、Enumeration
オブジェクトを返すelements()
メソッドが用意されています。クライアント側でこのEnumeration
オブジェクトを利用して、コレクションの反復処理を実行できます。
Vector names = new Vector(); names.add("Yossarian"); names.add("Minderbinder"); names.add("Clevinger"); for (Enumeration e = names.elements(); e.hasMoreElements(); ) { String name = (String)e.nextElement(); // ... }
当然ですが、この反復処理メカニズムがあるからといって、プログラマが昔ながらの反復処理を書けないわけではありません。
for (int i = 0; i < names.size(); i++) { String name = (String)names.get(i); // ... }
今でも一部の開発者は、この方式の反復処理を好むようです。パフォーマンスの点で、やや有利になる(可能性がある)と考えてのことです。しかし、これはオブジェクト指向に反するコードです。クライアント側が、基盤となるリスト表現形式を理解しなければ書けないコードだからです。Vector
を使用する場合は、そこまで知る必要はありません。また、プログラマがコレクションを順不同の構造(セットなど)に変えることに決めた場合、ループのコードを書き直す必要があります。
Java 2のコレクションクラスフレームワークによって、インターフェースと反復子を使用する堅牢なコレクションクラスセットがJavaに導入されました。Enumeration
がIteratorに置き換えられ、またメソッド名が簡略化されたため、コレクションをループ処理するコードがやや簡素になります。
List names = new ArrayList(); names.add("Yossarian"); names.add("Minderbinder"); names.add("Clevinger"); for (Iterator it = names.iterator(); it.hasNext(); ) { String name = (String)it.next(); // ... }
図1に、JDK 1.2~1.4でArrayList
として実装されたIteratorパターンのUML表記を示します。ここでは図を見やすくするため、中間の抽象クラスとインターフェースを省きました。
不思議なことに、今でもArrayList
ではなくVector
を選び、インターフェース型への代入という考え方を拒むJavaプログラマが見受けられます。知ってはいるけど、使わないという姿勢です。その理由が「10年来の習慣を変えるのは難しい」ではないとすれば、おそらく、Vector
を使うとデフォルトで有効になる同期のメカニズムが捨てられないのでしょう。しかし、これはコレクションフレームワークの同期ラッパーを使うことで解決できる問題です。私が耳にしたもう少し合理的な言い訳は、Vector
の増加量を制御したいから、というものです。これは新しいArrayList
クラスから抜け落ちた機能です。
リストの内部を移動する反復テクニックは、明らかに3種類では足りなかったようです。Java 5では、コレクションの内部を移動する別のメカニズムとして、強化版のfor
ループが導入されました。
List<String> names = new ArrayList<String>(); names.add("Yossarian"); names.add("Minderbinder"); names.add("Clevinger"); for (String name: names) { // ... }
強化版のfor
ループの利点は、クラスをパラメータ化された型にバインドできるため、クライアントコードでキャストする必要がなくなることです。さらにうれしいのは、Iterable
インターフェースを実装したクラスを書くことで、強化版のfor
ループのサポートを提供できることです。
class Menu implements Iterable<String> { private List<String> items = new ArrayList<String>(); public void add(String item) { items.add(item); } @Override public Iterator<String> iterator() { return items.iterator(); } }
Menu
にIterable
を実装すると、クライアント側はすべてのメニュー項目を反復処理するfor-each
ループを簡潔に記述できます。
Menu menu = new Menu(); menu.add("Beef Stroganoff"); menu.add("Filet Mignon"); menu.add("Rice with Tofu"); for (String item: menu) { // ... }
強化版のfor
ループの構文は、少なくともJava 7で導入の可能性があるクロージャの登場までは、Javaで記述できるほとんど限界に近い簡潔な表現です。