第1章
はじめてのデザインパターン  

第2章
逆引きカタログ ロジック編

第3章
逆引きカタログ J2EE編

第4章
逆引きカタログ その他

第5章
デザインパターン適用の勘所

第2章 逆引きカタログ ロジック編

Authors:Kondo Shuhei

Visiter (ビジタ)

 
イントロダクション
パターン解説
具体的な例
まとめ
 

イントロダクション

 ビジタパターンを一言で言うならば、「構造から処理を分離する」パターンです。
 では、処理を分離することでどのようなメリットがあるのでしょうか?
 処理の対象となるクラスの中に処理を書くことも可能ですが、対象となるクラスが非常に多い場合は、処理の追加や変更に対する労力は非常に大きなものとなります。
 ビジタパターンでは、処理の専門家のクラスを用意することでこの問題を解決します。
 処理クラスは構造内のオブジェクトを訪問(visit)し、処理を実行して回ります。 処理を受ける側のクラスは処理が来る玄関を作っておくだけで、あとは訪問を待てばよいのです。
 このように処理を構造から切り分けることで、処理は処理クラスの中に閉じ込め、処理の追加や変更に対する自由度を高めることができます。
TOP

パターン解説

 それではビジタパターンがどのようなしくみになっているか見ていきましょう。
 まずは、図14を見てください。
図14 ビジタパターン適用前/適用後



 ビジタパターンを使うことで、2つのクラスに分離していた処理が、1つの処理クラスに括り出されています。 こうしておくことで、処理内容に変更があった場合は、ElementA、ElementBの両方のクラスを変更しなくてもConcreteVisitorクラスを変更すればよいだけですし、新しい処理を追加したくなった場合は、Visitorクラスを継承した新しいクラスを作ることで対応できます。
 ではもう少し詳しく見てみましょう。 ビジタパターンにおいて大きな役割を持つのはビジタインタフェースとエレメントインタフェースです。 この2つのインタフェースが協調して、処理の分離を実現しています。
 ビジタインタフェースは処理を定義するクラスです。 すべての処理は、このインタフェースを継承し、具体的な処理クラスを実装していきます。 また、ビジタインタフェースでは、visit()メソッドを引数を変えて複数用意していますが、これは引数のクラスがElementAの場合とElementBの場合で、それぞれの処理を別々に分けて実装することが目的です。
 一方、エレメントインタフェースでは、処理をしてくれるビジタクラスを受け入れる玄関となるaccept()メソッドを定義しています。 accept()メソッドではビジタクラスに処理対象となる自分自身のインスタンスを渡し、それ以外のことはする必要はありません。 エレメントから見たビジタへの関わり合いを極力少なくすることで、ビジタの実装クラスの変更や追加に対するインパクトが小さくなります。
 このビジタとエレメントの関係は、しばしばダブルディスパッチと呼ばれます。 ダブルディスパッチとは「2重の呼び出し」という意味で、まずエレメントのaccept()メソッドを呼び出し、さらにビジタのvisit()メソッドを呼び出すという2つのメソッド呼び出しを経て、初めて行うべき処理が決まることからこう呼ばれます(図15)。
図15 ビジタパターンのシーケンス図
 このしくみは、水道管の修理をしてもらうのと少し似ています。 水道管が壊れたら、修理の専門家である修理工を呼び、玄関を開けておきます(accept)。 修理工は調査しに訪問してくれます(visit)。 ここでようやく、家によって異なる配管に対して、適切な水道管修理を行うことができるわけです(図16)。
図16 ビジタパターンのイメージ
 水道管を外して持って行ったり、自分で配管を調べたりする必要がないのは大きなメリットとなります。
 また、電気関係の故障だったら電力会社を呼び出したり、車の故障だったらディーラーを呼び出したりすることも可能ですが、修理の専門家を呼び出すだけで詳しい内容は知らなくてもすむのもメリットです。
TOP

具体的な例

 ビジタパターンでは、処理と構造を切り分けているので、複雑な構造を表現するコンポジットパターンの処理を行うには都合がよく、しばしばセットで使われます。
 そこで、ここでは先ほどの売り上げ集計処理をビジタパターンとコンポジットパターンを使って作ってみましょう。
 また、開発途中で総商品数のカウント処理も追加したくなったとします。 もしコンポジットパターンのみで総商品数カウント処理を実装しようとすると、Order、OrderItem、OrderLineクラスすべての実装を変更しなければなりません。
 しかし、ビジタパターンを使っていれば、ビジタクラスの実装を変更するだけで済みます(図17)。
図17 サンプルのクラス図
TOP

◎Element(リスト17

 すべての処理の対象となるクラスに適用するインタフェースです。
 処理を受け入れることを表します。
リスト17 Element.java

/**
 * 処理を受け入れるインタフェースです。
 */
public interface Element {
	void accept(Visitor visitor);
}

◎Order(リスト18

 すべての商品のスーパークラスです。
 Elementインタフェースを継承しています。
リスト18 Order.java

/**
 * すべての要素の基本となる抽象クラスです。
 */
public abstract class Order implements Element {

	/*
	 * 注文を追加します。
	 */
	public void addOrder(Order order) {
	}

	/*
	 * 料金を集計する抽象メソッドです。
	 */
	public abstract int getAmount();
}

◎OrderLine(リスト19

 Orderの入れ物となるクラスです。
 処理を受け入れるaccept()メソッドを実装しています。accept()メソッドでは、処理に対してvisit()メソッドを実行し、さらにOrderItemに対してaccept()メソッドで処理を渡しています。
リスト19 OrderLine.java

/**
 * 注文をまとめるクラスです。
 */
public class OrderLine extends Order {

	/* このクラスで管理する注文および商品のリストです。 */
	private List orders = new ArrayList();

	/*
	 * 商品や注文を追加します。
	 */
	public void addOrder(Order order) {
		orders.add(order);
	}

	/*
	 * 管理する注文や商品の値段を返します。
	 */
	public int getAmount() {
		int result = 0;

		for (Iterator itr = orders.iterator(); itr.hasNext();) {
			Order order = (Order) itr.next();
			result += order.getAmount();
		}

		return result;
	}

	/*
	 * 処理を受け入れます。
	 */
	public void accept(Visitor visitor) {
		visitor.visit(this);

		// 管理しているリストにも処理を渡して回る
		for (Iterator itr = orders.iterator(); itr.hasNext();) {
			Order order = (Order) itr.next();
			order.accept(visitor);
		}
	}
}

◎OrderItem(リスト20

 商品を表すクラスです。
 accept()メソッドで処理を受け入れ、処理に対してvisit()メソッドを実行します。
リスト20 OrderItem.java

/**
 * 商品クラスです。
 */
public class OrderItem extends Order {

	/* 商品の値段です。 */
	private int price;

	/*
	 * 値段を渡してクラスを初期化します。
	 */
	public OrderItem(int price) {
		this.price = price;
	}

	/*
	 * この商品の値段を返します。
	 */
	public int getAmount() {
		return this.price;
	}

	/*
	 * 処理を受け入れます。
	 */
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}

◎Visitor(リスト21

 すべての処理クラスが実装するインタフェースです。
 visit()メソッドは、要素によって処理が分けられるように、処理対象となるクラスを訪問するメソッドをそれぞれ引数に持っています。
リスト21 Visitor.java

/**
 * 処理を表すインタフェースです。
 */
public interface Visitor {

	/*
	 * OrderItemに対して処理を行います。
	 */
	void visit(OrderItem item);

	/*
	 * OrderLineに対して処理を行います。
	 */
	void visit(OrderLine line);
}

◎CalcVisitor(リスト22

 集計処理をするクラスです。
 このクラスでは処理の状態を保持できるので、処理を実行しながらその集計結果を蓄積しています。
 今回はOrderItemに対する処理だけですので、OrderLineに対する処理は実装していません。 OrderItemに対するvisit()メソッドが実行される度に、商品数のカウントアップと、値段の集計が行われます。
リスト22 CalcVisitor.java

/**
 * 集計処理をするクラスです。
 */
public class CalcVisitor implements Visitor {

	/* 金額の合計です。 */
	private int amount = 0;

	/* 総商品数です。 */
	private int itemCount = 0;

	/*
	 * OrderItemに対する処理を実行します。
	 */
	public void visit(OrderItem item) {
		amount += item.getAmount();
		itemCount++;
	}

	/*
	 * OrderLineに対する処理を実行します。
	 */
	public void visit(OrderLine line) {
		// 特に何もしない
	}

	/*
	 * 集計処理を返します。
	 */
	public int getAmount() {
		return amount;
	}

	/*
	 * 総商品数を返します。
	 */
	public int getCount() {
		return itemCount;
	}
}
TOP

まとめ

◎変更が容易なところと難しいところ

 ビジタインタフェースを継承したクラスを実装することで容易に処理の追加ができることは説明しましたが、では、エレメントの追加も容易でしょうか?
 答えは残念ながらノーです。 Visitorインタフェースをよく見てもらうとわかりますが、ビジタインタフェースでは、それぞれのエレメントクラスに対しての処理が個別のメソッドに定義されています。 もしエレメントクラスが増えたとしたら、ここに新しいメソッドを定義しなければないですし、当然すでに実装されたビジタクラスに対しても追加の実装をしなければなりません。
 このことから、ビジタパターンは「オブジェクトの構成要素は変化しないけど、処理をいろいろと増やし、変更したい」という場合に向いていると言えるでしょう。

◎処理に対して何を公開するのか

 ビジタパターンでは、処理を実装するクラスはその処理内容についてしか知らないので、処理に必要な情報は適宜エレメントクラスが提供してあげなければなりません。
 しかしここで注意して欲しいのは、処理クラスに対してエレメントクラスがどこまで情報を公開するか、ということです。
 エレメントクラスの情報をなんでもかんでも公開してしまうと、カプセル化が壊れたクラスになってしまいます。 かといって、処理に必要な情報は隠すわけにはいきません。 ビジタパターンを使う上で非常にジレンマを感じるところです。
TOP