ObjectSquare [2007 年 3 月号]

[技術講座]


事例で学ぶデザインパターン

第2回 Iteratorパターンの適用例を見て学ぼう

(株)オージス総研
福田 直樹

※雑誌『Java WORLD』2006 年 5 月号に掲載した記事のオリジナル原稿を Java WORLD 編集部の了解を得て掲載しています。


前回のおさらいと今回のお題

前回は、デザインパターンとは何か?という点から意義、学習の際のコツなどを見ていきました。簡単におさらいすると、デザインパターンとはソフトウェア設計のノウハウ集であり、それを活用することで効率よく品質のいいソフトウェアの構造を作ることができるというものでした。 今回からは、個別のデザインパターンを取り上げてアプリケーションに適用していきます。説明の流れは図1のような形式で行っていきます。デザインパターンを適用しない例では問題点を確認します。適用した例では、デザインパターンを適用しその問題点を解決していきます。最後に、取り上げたデザインパターンの基本構造を理解し、適用した例のどのクラスがどの構成要素になるかや、またポイントを確認しながらより理解を深めていただきたいと思います。今回は、Iterator パターンを取り上げることにします。

デザインパターン説明の流れ
図 1: デザインパターン説明の流れ

今回取り上げる機能

今回、Aくんが中心となって再構築しようとしているアプリケーションは、セミナー実施後に情報を登録する機能とその登録した情報に関する様々な照会機能があるものです。Aくんが最初に取り掛かっている機能は「受講情報照会機能」です。これは、登録した受講者の全情報を照会したり、受講時にアンケート評価が悪かった受講者には月末に何らかのフォローをするために情報を蓄えておき、その情報も照会できるようになっています。それらの情報を元にその顧客へメールニュースを配信したりする元データとして使っているようです。また、照会した結果の画面は図2のようなものです。受講に関する情報と共に、顧客の情報を表示するようになっています。

受講情報照会画面
図 2: 受講情報照会画面

デザインパターンを適用しない場合

Cさん「まずは、受講情報照会機能だね。今までのソースコードも確認してくれたんだよね?」」
Aくん「はい。確認して、実際に実装をやり始めています。」
Cさん「すばやいね。ただ、いきなりソースコードを書くのではなく、今後は設計モデルとして最低限クラス図を作成することにしよう。当然ソースコードは重要なんだけど、特に複数人で開発を行うのであれば、開発者間でコミュニケーションを取る時に、設計モデルで話をした方が早いんだ。」
Aくん「わかりました。」
Cさん「うん、モデルは追々作成していくことにしよう。で、実装している途中のソースコードはあるのかい?」
Aくん「はい。(リスト1)」
リスト1:Iterator パターンを適用前の各クラス
/***** ParticipationList.java *****/
package domain;

import java.util.ArrayList;
import java.util.List;

public class ParticipationList {
	
	private List participations;
	
	public ParticipationList() {
		this.participations = new ArrayList();
	}
	public void add(Participation participation) { 
		this.participations.add(participation);
	}

	// List型を返却する
	public List getParticipations() {
		return this.participations;
	}

	// 。。。その他、省略。。。
}

/***** FollowList.java *****/
package domain;

public class FollowList {
	
	private Participation[] participations;
	private int last = 0;
	
	public FollowList(int max) {
		this.participations = new Participation[max];
		this.last = 0;
	}

	public void add(Participation participation) { 
		this.participations[last] = participation;
		this.last++;
	}

	// 配列で返却する
	public Participation[] getParticipations() {
		return this.participations;
	}

	// 。。。その他、省略。。。
}

/***** Participation.java *****/
package domain;

public class Participation {
	
	private String id;
	private Customer customer;
	private Seminar seminar;
	private Comment comment;

	private String date;
	private int price;
		
	public Participation() {} 
	
	public Participation(String id, Customer customer, Seminar seminar, Comment comment, String date, int price) {
		this.id = id;
		this.customer = customer;
		this.seminar = seminar;
		this.comment = comment;
		this.date = date;
		this.price = price;
	}

	// 。。。その他、省略。。。

}

/***** PTReferFrame.java *****/
package gui;

public class PTReferFrame extends JFrame {

	// 。。。省略。。。

	//「照会」ボタンが押されたときの処理
	private void doSearch() {
		
		// 。。。省略。。。
		
		// 「受講リスト」が選択された場合。(PLISTは定数で宣言させているものとする)
		if(selectedList == PLIST) {
			// List型で集合を取得する
			List list = participationList.getParticipations();

			// List型に格納されたParticipationオブジェクトを取得し、テーブルに設定
			for(int i=0;i<list.size();i++) {
				
				// Participaiton オブジェクトを取得
				Participation participation = (Participation)list.get(i);
				
				// 表にデータを追加する。
				// 。。。省略。。。
			}
		//「フォローリスト」が選択された場合。(FLISTは定数で宣言させているものとする)
		} else if (selectedList == FLIST) {
			// 配列型で集合を取得する
			Participation[] participations = followList.getParticipations();
			
			// 配列に格納されたParticipationオブジェクトを取得し、テーブルに設定
			for(int i=0;i<participations.length;i++) {
				if(participations[i] != null) {
					// Participaiton オブジェクトを取得
					Participation participation = participations[i];
					
					// 表にデータを追加する。
					// 。。。省略。。。
				}		
			}
		}
	}
}

リスト1を見ると、受講情報である Participation クラスが Customer オブジェクトと Seminar オブジェクト、Comment オブジェクトを保持しています。そして、ParticipationList クラスは全受講オブジェクトを List 型で保持しており、FollowList クラスはフォローが必要な固定人数の受講者オブジェクトを配列で保持しています。GUI 側の PTReferFrame クラスからそれらの情報を取得するようにしています。

Cさん「なかなかよくできてるじゃないか。いい感じだね。何か実装をしてて気になるところはなかったかい?まずは、改善できそうなポイントがあれば、それをチェックしていこう。改善策は後で考えようか。」
Aくん「PTReferFrame で取得した情報をテーブルに設定する部分で、同じようなループのコードが 2 箇所出てきてしまってるところが少し気になってます。うまくまとめられなかったので。」
Cさん「リスト1の太字の部分だね。ここは、FollowList と ParticipationList でループが別々になっているんだね。これは、Participation のデータの持ち方がそれぞれ違うことが影響しているようだけど。」
Aくん「FollowList クラスには配列アクセスの操作が既存のソースコードの内部に結構あるんです。それらは再利用したいと思っていたので、あまり変えたくないんですが。ParticipationList クラスについても同じです。」

各リストクラスでは getParticipations() メソッドが提供されており、それによって取得したデータの要素をクライアント側で for() ループによって参照しています。ここで注目する点は、その保持するデータ構造をクライアントが意識してプログラミングしてしまっている点です。図 3 は FollowList(配列)の場合ですが、配列に対しての length() メソッドや getParticipations() メソッドの戻り値である配列に依存したプログラミングしていることがわかります。

配列を扱って処理をするクライアントの呼び出し形式(Iteratorパターン適用前)
図 3: 配列を扱って処理をするクライアントの呼び出し形式(Iteratorパターン適用前)
Aくん「リストクラス側のソースコードを変えない限り、クライアント側はこのまま行くしかないですね。」
Cさん「この点については、まずは改善できそうなポイントとして押さえておこうか。別の視点から見ると、現状のもので、もしリリースした後に FollowList クラスが保持する Participation 配列を Java のコレクションクラスで置き換えたい場合はどうする?コレクションクラスは便利な機能もたくさんあるからそういう事態も考えられるからね。」
Aくん「FollowList クラスのソースコードは変えたくないですが、、でも、変更は局所的なのでなんとかできそうです。あ、あとそれを使ってるクライアントは修正が必要ですね。」
Cさん「その通り。FollowList にアクセスしているクライアントを探し出して、変更しないといけなくなるよね。」
Aくん「確かにそうですね。Participation オブジェクトを取得して何か処理を行っている部分はかなりの部分で変更が発生しそうです。受講情報を取得してクライアント側で何か処理を行うことはありそうなので。影響範囲が広いですね。」
Aくん「『各リストクラスのソースコードを変えないで、かつ呼び出し側のクライアントのアクセス方法を統一する』ような都合のいい改善策はあるんでしょうか?」
Cさん「ちょっとこの点は押さえておくことにして、その他の点を見てみようか。集合を扱うようなクラスではデータの取り出しルールや取り出し順をいろいろなパターンで持つことが多いものだよね。既存のソースコードでもそういう部分はなかったかい?」
Aくん「 ええ、ありました。各リストクラスにメソッドがいろいろとあった気がします。」
Cさん「例えば、取り出しルールが追加されたら各リストクラスにどんどんメソッドが追加されていく形になるね。各リストクラスが肥大化してしまうと、柔軟性が失われてしまう。これも改善ポイントに入れてしまおう。」

Iteratorパターンの適用

以上で今回の問題点が出揃ったようです。Cさんは、ここまでの話を整理するために、A君のノートを開き、ペンを手に取りました。

  1. 集約オブジェクト(各リストクラス)が保持するデータ構造を意識してクライアントはアクセスしている(呼び出し側のオブジェクトと集合を管理するオブジェクトが密に結合している)
  2. 集約オブジェクトはデータ管理の責務とそのデータ走査の責務を保持している

Aくん「はい。この点は理解しました。」
Cさん「あと、以下の c 点についても対応できるとそれを利用する側からすると便利だから考えてみよう。」
Cさんは、b の下に以下のような項目を書き足しました。

  1. 走査の方法を簡単に切り替えられるようにする

Cさん「これらの点に対応できるパターンとして、Iterator パターンがあるんだ。Iterator パターンは簡単に言うとデータ保持・管理のクラスとは別に走査のためのクラスとして Iterator クラスを抽出するんだ。順を追って改善していこう。まず a の点に関しては、getParticipations() メソッドは現在データ構造に依存した型を返却しているから、これを直接クライアントから使わないように変更する必要がある。また、b の点に関しては、集約オブジェクトである ParticipationList クラスの責務が大きくなってしまっているから、その責務を軽くしたいわけだね。」
Aくん「そうですね。でも、やっぱりリストクラスのソースコードを変更するんじゃないですか?」
Cさん「まあ、慌てないで。Iterator パターンを適用した構造をクラス図で見てみようか。」
変動要素の分離
図 4: Iterator パターンを使った場合のクライアントの呼び出し形式

図4を見てください。FollowList を走査するための、FollowListIterator クラスと ParticipationList を走査するための ParticipationListIterator クラスを導入しています。それぞれの Iterator クラスには、集約オブジェクトの要素にアクセスするためのメソッドを定義し、各要素をどこまで走査したかを記録するための走査子(cusor)を保持しています。用意するメソッドは、集約するオブジェクトの次の要素が存在するかどうかを boolean で返す hasNext() メソッドと、次の要素を Object 型で返す next() メソッドです。リスト 2 は FollowListIterator のソースコードを示しています。また、データを走査する責務をそれぞれの Iterator クラスに割り当てたため、各リストクラスの責務を軽くすることができそうです。さらに、各リストクラスの共通の Aggregate インタフェースを抽出しました。

リスト2:Iterator パターンを適用後の具象 Iterator(FollowListIterator)クラス
/***** FollowListIterator.java *****/
package domain;

public class FollowListIterator implements Iterator{

	private Participation[] followList;
	private int cusor;
	
	public FollowListIterator(FollowList followList){
		this.followList = followList.getParticipations();
		cusor = 0;
	}
	
	public boolean hasNext(){
		if(followList.length > cusor && followList[cusor] != null){
			return true;
		}else{
			return false;
		}
	}

	public Object next(){
		Participation participation =  followList[cusor];
		cusor++;
		return participation;
	}

}

Iterator パターンを使った際のクライアント側のソースコードを見てみましょう。(リスト 3 )以前は、集約するデータ構造に依存していたものが、Iterator を介することで全く依存しないソースコードになっています。クライアント側では、Iterator に対して hasNext() で「次あるかどうか」を確認し、あれば「その要素をください」と要素を取得しています。Iterator が「クライアント」と「集約オブジェクト」との仲介役というわけです。その分 Iterator は集約オブジェクトが保持するデータ構造を意識したコーディングになります。呼び出し形式としては図 5 のようになります。

リスト3:Iterator パターンを適用後の PTReferFrame からの呼び出し部分
/***** PTReferFrame.java *****/
package gui;

public class PTReferFrame extends JFrame {

	// 。。。省略。。。

	//「照会」ボタンが押されたときの処理
	private void doSearch() {
		
		// 。。。省略。。。

		// イテレータを取得。Aggregateインタフェースに対してiterator()メソッドを呼び出している。
		Iterator iterator = aggregate.iterator();

		// イテレータに対して「次はある?」と確認。
		// 集約オブジェクトが保持するデータ構造を意識しないため、ループは統合された。
		while (iterator.hasNext()) {
			// Participaiton オブジェクトを取得
			Participation participation = (Participation)iterator.next();
			
			// 表にデータを追加する。
			// 。。。省略。。。
		}
	}

}
Iterator パターン適用後のクラス図
図 5: Iterator パターン適用後のクラス図

次に、集約オブジェクトである 2つのリストクラスにも少し修正が必要です。これらを使用するクライアントに対して Iterator オブジェクトを返却するように iterator() メソッドを追加する必要があります。しかし、各リストクラスに対して行う変更というのはこれだけです。他のソースコードは変更する必要がありません。Iterator は様々なクライアントから同時に呼び出される可能性があるため、走査するクライアント毎に Iterator を生成する必要があります。そのため、iterator() メソッドの中では対応した具象 Iterator オブジェクトを生成しています。リスト4に変更後の FollowList クラスを示します。

リスト4:Iterator パターンを適用後の FollowList クラスへの変更
(iterator() メソッドの追加)
/***** FollowList.java *****/
public class FollowList implements Aggregate {
	
	// 。。。省略。。。

	// iterator()メソッドを追加
	public Iterator iterator() {
		return new FollowListIterator(this);
	}
}

このままでも今まで考えてきた問題点は解決されています。しかし、この構造をさらに柔軟性の高い構造にするためには、具象 Iterator クラスのインタフェースを定義します。そうすることによって、呼び出し側のクライアントとサービスの実装を提供する具pp象クラスとを分離することができます。 ここでは、ParticipationList クラスについても対応する具象 Iterator クラスを定義しましたが、Java では標準の Iterator クラスが提供されています。そのため、Iterator の共通インタフェースを java.util.Iterator インタフェースとして定義するとよいでしょう。その場合は、ParticipationList クラスの iterator() メソッドは以下のようになり、ParticipationListIterator クラスは必要なくなります。(※注釈A)

(※注釈A)最初に ParticipationListIterator クラスを抽出したのは Iterator パターンの基本を説明するためと考えてください。

public java.util.Iterator iterator() {
	this.participations.iterator();
}

c の点にも対応できやすい構造になっていることは理解できるでしょう。Iterator に他の走査方法を追加するような場合は、別の走査をするメソッドを提供するか、サブクラス化して機能を追加してもいいでしょう。また、別の走査行う様々なイテレータ実装を提供することも考えられます。

別の走査方法を提供することで、特定の条件に一致するようなデータのみ取り出すような Iterator やソート機能を果たすような Iterator も集約オブジェクトとは別に提供するようなことが可能です。その場合も hasNext()、next() メソッドの実装でデータを順に取り出していけばいいわけです。図6に複数の具象 Iterator クラスを導入したイメージを示しておきます。この点は、前回解説したように、変動要素(この場合は走査方法)は別クラスとして切り離し、変更する際の柔軟性を向上させるという指針が適用できている例でもあります。

Iterator パターンの基本クラス構造
図 6 :Iterator パターンの基本クラス構造

Iterator パターンの基本構造

イテレータとは、「反復子」や「走査子」と言われるものです。集約するオブジェクトの各要素に順番にアクセスするためのデザインパターンです。 Iterator パターンを考える際のポイントとしてはこう言えるでしょう。

それによって、主に以下のようなメリットが得られます。

これらは別の言い方をすると、「クライアント側での要素への処理」や「走査方法」に関して、再利用性が高まっているとも言えます。 前回でも解説しましたが、これらの点はクラスの責務分割の指針の1つであり、間接層を導入してクラス間の結合度を弱めることでソフトウェアの品質を高めようという試みです。Iterator オブジェクトが間接層の役割を果たしているわけです。図7と表1に Iterator パターンの基本構造と適用例のクラスとの対応を示しておきます。

複数の具象Iteratorクラスを導入した例
図 7 :複数の具象Iteratorクラスを導入した例

表1:Iterator パターンの構成要素
構成要素意味適用例での対応するクラス
Aggregate集合を管理し、Iteratorオブジェクトを生成するためのインタフェースAggregate
ConcreateAggregateAggregateインタフェースを実装するクラスParticipationList、FollowList
Iterator走査、データの取り出しを行うためのインタフェースIterator
ConcreateIteratorIteratorインタフェースを実装するクラスFollowListIterator(、ParticipationListIterator)
ElementConcreateAggregateで管理される要素クラスParticipation

初めてデザイン・パターンの適用を試みたA君でしたが、Cさんのお陰で、今回はなんとか理解できたようです。皆さんも同じようなソースコードを見つけた場合は、適用を検討してみてください。

《参考文献》

  1. 「オブジェクト指向における再利用のためのデザインパターン」
    著者:Erich Gamma, Rechard Helm, Ralph Jonson, John Vlissides
  2. 「Java言語で学ぶデザインパターン入門」
    著者:結城 浩

記事の内容を5点満点で評価してください。
1点 2点 3点 4点 5点
記事に関するコメントがあれば併せてご記入ください。

© 2007 OGIS-RI Co., Ltd.
Prev Index Next
Prev. Index Next