Processingで学ぶ 実践的プログラミング専門課程

第24回デザインパターン(2) Iterator

導入

Processingがバージョン3.0(2015年8月29日現在3.0b5)となり、IDEのインタフェイスが大きく変更されました。シンプルで使いやすいように感じます。アイコンも洒落た感じになりました。ただ、テキストエディタの挙動がもう少し洗練されていなかったり、機能面で後方互換ではない部分があります。新しいバージョンのProcessingでは、これまでに作成したProcessingのsketchが動かない場合があるということです。ここで学習する範囲では大きな問題はないと考えていますが、記憶にとどめてください。

Processing3についてのダイアログ
画像

閑話休題。今回はGoFによるデザインパターン23種類のうち、おそらく最も用いられているパターン、Iteratorを学習します。1つひとつ、順に要素を取り扱うという、コンピュータの最も得意とする機能を、上手におこなう目的でまとめられたパターンです。

展開

Iterator(イテレータ)

前述したように、IteratorパターンはGoF本に紹介されている23個のパターンの中で、最も身近なデザインパターンです。現在の高級言語を利用する若いプログラマは、Iteratorパターンを使ったクラスを無意識のうちに使っていることでしょう。

Iteratorパターンの目的

iterateは「繰り返される、繰り返す」の意味がある動詞で、これを名詞化したのがIterator、つまり「繰り返されるもの、繰り返すもの」の意味になります。

オブジェクトの集合を取り扱う際、オブジェクトを配列に代入し、添字をインクリメント(一度に1つ増やすこと)することで巡回する方法が最も原理的です連載第9回⁠。

連載第9回で掲載した配列を利用するコード
String names[] = {"Tim Bray",        // 0番目の要素
                  "Brian Kernighan", // 1番目の要素
                  "Jon Bentley",     // 2番目の要素
                  "Karl Fogel"};     // 3番目の要素

for(int i = 0; i < names.length; ++i){
  println(names[i]);
}

Java言語、そしてProcessingではデータの集合に先頭から順にアクセスする手段としてIteratorパターンの使用を推奨しています。上のコード例のように配列を取り扱うfor文は、Iteratorパターンを使用する拡張for文で、次のように書き換えられます。

先のfor文を拡張for文で書き直したところ
for(String v : names){   println(v); }  

上のコード例ではIteratorパターンの存在が隠されています。このコードの裏ではIteratorパターンを利用して配列の要素の先頭から順にアクセスしています。

Iteratorパターンを使うと、データがどんな仕組みで保管されているかは、データを必要とする側のコードで意識しないで済みます。上のコード例で言えば、

  • names[i]として、配列というデータ構造の変数namesの、
    先頭からi番目に納められたString型のデータ」

とアクセスしていたところが、単に

  • String型のデータv

で済みます。Iteratorパターンに従っていることが分かれば、データがどんな仕組みで保管されていようとも、決まったメソッドを呼べば目的のデータにアクセスできるように仕組まれています。これを専門用語を使って言えば「データに順番にアクセスする仕組みを抽象化した」となります。

[Iteratorパターン]
複数のオブジェクトを順に取り扱う機能を抽象化したもの。

Java言語やProcessingでは、配列を始め多くのコレクションクラスでIteratorパターンが適用されており、先ほどの拡張for文のようにシンプルなコードで要素にアクセスできます。

GoFによるIteratorパターンの全体像

ここでは、GoFによるIteratorパターンが、どのようにまとめられているのかを見ていきましょう。

Java言語のクラスライブラリ中で実装されているIteratorパターンは、GoF本に紹介されている各クラスとクラス名、インタフェイス名が異なる部分がありますが、基本的な考え方と構造は同じです。

下図はIteratorパターンを用いたコードのクラス図の一例です。

Iteratorパターンのクラス図
画像

Iteratroパターンでは、ConcreteAggregateクラスに納められたデータの集合をClientクラスが順に取り出して処理することを目的とします。図の一番下にあるDataクラスが、ConcreteAggregateクラスが保持するデータ保管用クラスです。Aggregateという英語は「集合」とか「集団」といった意味があります。Concreteは「具体的な実体」です。

Aggregateインタフェイスのiteratorメソッドは、Iteratorインタフェイスを実装したConcreteIteratorオブジェクトを返します。ConcreteIteratorConcreteAggregateに納められたオブジェクト群にアクセスするコードを持っています。

結局Clientクラスがデータのアクセスに必要とするのは、ConcreteAggregateオブジェクトと、ConcreteIteratorオブジェクトの参照を持つIteratorインタフェイスのオブジェクトです。

[Clientクラスがデータアクセスに必要とする情報]
  • ConcreteAggregateオブジェクトへの参照
  • ConcreteAggregateオブジェクトのiteratorメソッドが返すConcreteIteratorへの参照

Iteratorパターンのメリット

この仕組みがあれば、ConcreteAggregateクラスがどんな型やクラスのオブジェクト群を保持していても、それに関係なくClientクラスから、Iteratorインタフェイスの持つhasNextメソッドやnextメソッドでオブジェクト群の巡回が可能になります。仮にConcreteAggregateクラスが保持するオブジェクトの型が変更されても、それに対応してConcreteIteratorクラスのコードを修正しておけば、Clientクラスのコードでは、ConcreteAggregateクラスの保持するデータの巡回のためのコードに変更が必要ありません。

チームでコーディングしている際に、Iteratorパターンに従うことを共通理解しておけば、ConcreteAggregateクラス側の開発者も、それを用いるClientクラス側の開発者も、互いのコードに深く立ち入らずに独立して開発が続けられるのです。

例えば、

「このクラスはIteratorパターンに従っています」

と言えば、デザインパターンを知るプログラマは、そのクラスにはiteratorメソッドがあり、それを呼ぶとConcreteIteratorのオブジェクトを取得できるとわかります。後は、次々とオブジェクト群を巡回する際に、nextメソッドでオブジェクトを取得し、hasNextメソッドで次のオブジェクトが取得可能かを知ることができます。

Java言語では、既にConcreteIteratorにあたるオブジェクトを返すメソッドiteratorを実装しているクラスArrayListVectorなど)があります。Java言語において、iteratorメソッドを実装しなさいよ、ということを規定するAggregateインタフェイスにあたるのが、Java言語ではIterable<T>インタフェイスです。

ですから、Iteratorパターンの全体像を知らなくても、例えば拡張for文を使ったことがあるならば、Iteratorパターンを使ってオブジェクト群の巡回をしたことがあるということです。プログラマがConcreteIteratorクラスを書かなくても、Java言語側で用意していますから、Clientクラスからオブジェクトを巡回するコードを簡単に書けるのです。

Java言語やProcessingで使われているIteratorパターンを見る

次のサンプルコードを見てください。

import java.util.Arrays;
import java.util.Iterator;
ArrayList<String> A = new ArrayList(Arrays.asList("Tim Bray",
              "Brian Kernighan",
              "Jon Bentley",
              "Karl Fogel"));
println("**Iteratorパターンを使わない**"); // (1)
for(int i = 0; i < A.size(); i++){
  println(A.get(i));
}
println("**拡張for文。内部でIteratorパターンを使っている**"); // (2)
for(String v : A){
  println(v);
}
println("**明示的にIteratorパターンを使う**"); // (3)
Iterator it = A.iterator();
while(it.hasNext()){
  println(it.next());
}

Stringクラスのデータを納めるArrayListオブジェクトAを宣言し、4つの要素を納めます。これら4つの要素を順にコンソールに出力する3つの方法を見ていきます。

(1)は、昔ながらの配列の添字を指定した方法です。配列の添字となる変数iを用いて要素を指定します。

(2)は、拡張for文を用いて巡回する方法です。拡張for文は配列の添字やIteratorが表に現れません。拡張for文の向こうではIteratorパターンを使用しています。オブジェクトの集合がIteratorインタフェイスを実装しているという前提があるからこそ、拡張for文のような簡略表現が可能です。

(3)は、明示的にIteratorパターンを使用して巡回するループです。拡張for文は、内部では(3)のような処理を行っています。単純に先頭から末尾まで巡回するだけのために、(3)のようなコードを書くのは無駄です。(2)のように書くほうがシンプルで良いでしょう。

注目してもらいたいのは、(3)の場合には巡回と表示に使用するデータの型やクラスを指定していないということです。(1)も(2)も、表示する要素がString型のArrayListAである」とか、String型のオブジェクトvである」などと具体的な型やクラスを指定しています。しかし、(3)の場合には、AからConcreteIteratorオブジェクトへの参照を取得した後は、その参照だけを使用しています。巡回や表示にあたってはオブジェクトの型は不問になるため、ConcreteIteratorオブジェクトの取得先を切り替えれば、巡回と表示のためのコードは全く同じものが使えるというわけです。多様な型のデータをループ処理したい場合には、(3)のコードの書き方にメリットがあります。

あなたのコードにGoFのIteratorパターンを使う

それでは、GoFのIteratorパターンを具体的なコードにしてみます。連載第20回演習問題2の解答例に示したコードにIteratorパターンを適用します。その際、sketch名を改めてADrawToolとします。

Iteratorパターンを適用したsketch

これにより、ExStatechartDiagram2.pdedrawメソッド内のコードでは隠れていたIteratorが、ADrawTool.pdeでは表に引っ張りだされました。

ExStatechartDiagram2.pdeより
  for(Point p : dots){
    point((int)p.getX() * rate,(int)p.getY() * rate);
  }
ADrawTool.pdeより
  Iterator it = ca.iterator();
  while(it.hasNext()){
    Point p = (Point)it.next();
    point((int)p.getX() * rate,(int)p.getY() * rate);
  }

これらのサンプルコードでは、XY座標値をPointクラスのオブジェクトpに格納して点を描画するだけです。ExStatechartDiagram2.pdeでは、もともとArrayListが持っていたJava言語の持つIteratorパターンを利用した拡張for文を使ってデータを巡回するだけで事足りていました。

caはGoFによるIteratorパターンのAggregateインタフェイスを実装したConcreteAggregateクラスのオブジェクトですから、iteratorメソッドを持ちます。このiteratorメソッドを呼ぶと、Iteratorインタフェイスを実装したConcreteIteratorクラスのオブジェクトを返します。Iteratorインタフェイスのオブジェクトitが持つConcreteIteratorオブジェクトへの参照を用いて、whileループ内で各点の示すディスプレイウインドウ内の座標に点を描きます。

GoFによるデザインパターンのIteratorを忠実に適用したので、ADrawTool.pdeのコードは冗長になっています。これだけではIteratorパターンを見える化した利益はありません。

このADrawTool.pdeを発展させ、CADソフトウェアにしようとすると、単純な順方向イテレータだけでは不自由になるでしょう。例えば、拡張for文やJava言語に用意されているIteratorインタフェイスを使っていると、逆順の巡回ができません。ですから、より拡張されたIteratorインタフェイスがほしくなります。

それでは独自のIteratorパターンを用いるメリットがある活動に入ります。データ群を逆順に巡回するための、hasPrevious(⁠⁠1つ前の要素はある?⁠⁠)とprevious(⁠⁠1つ前の要素を取得⁠⁠)メソッドを作りましょう。逆順の巡回をスムーズに行うためには、データの末尾へ移動するメソッドがあると便利です。これをgoTailメソッドとしましょう。それならば、先頭からの巡回をスムーズに行うために、データの先頭へ移動するメソッドも用意しましょう。これをgoHeadメソッドとします。これらをインタフェイスIteratorに用意し、ConcreteIteratorへ実装しましょう。これらのメソッドを実装するクラスは、内部がどのように実装されていようとも、Client側にあたるADrawToolのコードからはIteratorインタフェイスを通じて、同じようにcaにアクセスできます。Iteratorパターンを利用する側のコードは、シンプルになり、なおかつIteratorパターンを適用したクラス側はコードの部品化度が高まるのです。

演習

演習1(難易度:middle)

sketchADrawToolIteratorインタフェイスにhasPrevious, previous, goHead, goTailの各メソッド宣言を用意し、ConcreteIteratorクラスにそれらを実装しましょう。

まとめ

  • Iteratorパターンのメリットと使い方を学びました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. Iteratorパターンのメリットがわかりましたか?
    1. わかりました。自分のプログラミングにも活用したい。
    2. 解説を理解することはできたが、それほどメリットを感じない。
    3. 本文が理解できない。
  2. Iteratorパターンを使えるようになりましたか?
    1. 使えるようになった。自分のプログラミングにも活用できそうだ。
    2. 本文の例を理解することはできたが、自分のプログラミングに活用できる気がしない。
    3. 本文の例が理解できない。

参考文献

  • 『増補改訂版Java言語で学ぶデザインパターン入門』⁠結城浩 著、ソフトバンククリエイティブ
    • 誰もが認める最も分かりやすいデザインパターン入門書。本来は原典(GoF本)を勧めるべきところですが、本書は別格。
  • 『オブジェクト指向における再利用のためのデザインパターン』⁠Eric Gamma 著、ソフトバンククリエイティブ
    • デザインパターンの原典。別称『GoF本』サンプルコードがC++で書かれているため、ProcessingやJava言語のユーザにはハンディがあります。しかし結城浩氏の入門書を読みこなした後ならば本書にあたる価値があります。
  • 『Java デザインパターン徹底攻略』⁠日立ソフトウェアエンジニアリング⁠株⁠インターネットビジネス部 著、技術評論社
    • 流石の日立。前掲のGoF本の解説書で、サンプルはJava言語。絶版であることが惜しまれます。強く再版を望みます。

演習解答

  1. 追加したhasPrevious, previous, goHead, goTailが動作するかをチェックするテストコードConcreteIteratorTest.pdeを追加しています。

おすすめ記事

記事・ニュース一覧