第2章 逆引きカタログ ロジック編
Authors:Kondo Shuhei
ビジタパターンを一言で言うならば、「構造から処理を分離する」パターンです。
では、処理を分離することでどのようなメリットがあるのでしょうか?
処理の対象となるクラスの中に処理を書くことも可能ですが、対象となるクラスが非常に多い場合は、処理の追加や変更に対する労力は非常に大きなものとなります。
ビジタパターンでは、処理の専門家のクラスを用意することでこの問題を解決します。
処理クラスは構造内のオブジェクトを訪問(visit)し、処理を実行して回ります。
処理を受ける側のクラスは処理が来る玄関を作っておくだけで、あとは訪問を待てばよいのです。
このように処理を構造から切り分けることで、処理は処理クラスの中に閉じ込め、処理の追加や変更に対する自由度を高めることができます。
それではビジタパターンがどのようなしくみになっているか見ていきましょう。
まずは、図14を見てください。
図14 ビジタパターン適用前/適用後
ビジタパターンを使うことで、2つのクラスに分離していた処理が、1つの処理クラスに括り出されています。
こうしておくことで、処理内容に変更があった場合は、ElementA、ElementBの両方のクラスを変更しなくてもConcreteVisitorクラスを変更すればよいだけですし、新しい処理を追加したくなった場合は、Visitorクラスを継承した新しいクラスを作ることで対応できます。
ではもう少し詳しく見てみましょう。
ビジタパターンにおいて大きな役割を持つのはビジタインタフェースとエレメントインタフェースです。
この2つのインタフェースが協調して、処理の分離を実現しています。
ビジタインタフェースは処理を定義するクラスです。
すべての処理は、このインタフェースを継承し、具体的な処理クラスを実装していきます。
また、ビジタインタフェースでは、visit()メソッドを引数を変えて複数用意していますが、これは引数のクラスがElementAの場合とElementBの場合で、それぞれの処理を別々に分けて実装することが目的です。
一方、エレメントインタフェースでは、処理をしてくれるビジタクラスを受け入れる玄関となるaccept()メソッドを定義しています。
accept()メソッドではビジタクラスに処理対象となる自分自身のインスタンスを渡し、それ以外のことはする必要はありません。
エレメントから見たビジタへの関わり合いを極力少なくすることで、ビジタの実装クラスの変更や追加に対するインパクトが小さくなります。
このビジタとエレメントの関係は、しばしばダブルディスパッチと呼ばれます。
ダブルディスパッチとは「2重の呼び出し」という意味で、まずエレメントのaccept()メソッドを呼び出し、さらにビジタのvisit()メソッドを呼び出すという2つのメソッド呼び出しを経て、初めて行うべき処理が決まることからこう呼ばれます(図15)。
このしくみは、水道管の修理をしてもらうのと少し似ています。
水道管が壊れたら、修理の専門家である修理工を呼び、玄関を開けておきます(accept)。
修理工は調査しに訪問してくれます(visit)。
ここでようやく、家によって異なる配管に対して、適切な水道管修理を行うことができるわけです(図16)。
水道管を外して持って行ったり、自分で配管を調べたりする必要がないのは大きなメリットとなります。
また、電気関係の故障だったら電力会社を呼び出したり、車の故障だったらディーラーを呼び出したりすることも可能ですが、修理の専門家を呼び出すだけで詳しい内容は知らなくてもすむのもメリットです。
ビジタパターンでは、処理と構造を切り分けているので、複雑な構造を表現するコンポジットパターンの処理を行うには都合がよく、しばしばセットで使われます。
そこで、ここでは先ほどの売り上げ集計処理をビジタパターンとコンポジットパターンを使って作ってみましょう。
また、開発途中で総商品数のカウント処理も追加したくなったとします。
もしコンポジットパターンのみで総商品数カウント処理を実装しようとすると、Order、OrderItem、OrderLineクラスすべての実装を変更しなければなりません。
しかし、ビジタパターンを使っていれば、ビジタクラスの実装を変更するだけで済みます(図17)。
すべての処理の対象となるクラスに適用するインタフェースです。
処理を受け入れることを表します。
リスト17 Element.java
public interface Element {
void accept(Visitor visitor);
}
すべての商品のスーパークラスです。
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);
}
}
すべての処理クラスが実装するインタフェースです。
visit()メソッドは、要素によって処理が分けられるように、処理対象となるクラスを訪問するメソッドをそれぞれ引数に持っています。
リスト21 Visitor.java
public interface Visitor {
void visit(OrderItem item);
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;
public void visit(OrderItem item) {
amount += item.getAmount();
itemCount++;
}
public void visit(OrderLine line) {
}
public int getAmount() {
return amount;
}
public int getCount() {
return itemCount;
}
}
◎変更が容易なところと難しいところ
ビジタインタフェースを継承したクラスを実装することで容易に処理の追加ができることは説明しましたが、では、エレメントの追加も容易でしょうか?
答えは残念ながらノーです。
Visitorインタフェースをよく見てもらうとわかりますが、ビジタインタフェースでは、それぞれのエレメントクラスに対しての処理が個別のメソッドに定義されています。
もしエレメントクラスが増えたとしたら、ここに新しいメソッドを定義しなければないですし、当然すでに実装されたビジタクラスに対しても追加の実装をしなければなりません。
このことから、ビジタパターンは「オブジェクトの構成要素は変化しないけど、処理をいろいろと増やし、変更したい」という場合に向いていると言えるでしょう。
◎処理に対して何を公開するのか
ビジタパターンでは、処理を実装するクラスはその処理内容についてしか知らないので、処理に必要な情報は適宜エレメントクラスが提供してあげなければなりません。
しかしここで注意して欲しいのは、処理クラスに対してエレメントクラスがどこまで情報を公開するか、ということです。
エレメントクラスの情報をなんでもかんでも公開してしまうと、カプセル化が壊れたクラスになってしまいます。
かといって、処理に必要な情報は隠すわけにはいきません。
ビジタパターンを使う上で非常にジレンマを感じるところです。