第2章 逆引きカタログ ロジック編
Authors:Kondo Shuhei
アプリケーションで扱うデータをモデル化してみると、オブジェクト同士の関係を単純な1対1の関係だけで管理できることはほとんどありません。
多くのオブジェクトは、他のいくつかのオブジェクトへの関わりを持つことで構造を作っています。
この構造が単純なうちはよいのですが、また別のオブジェクトへと次々に関わりを持っていくことで、構造はどんどん複雑になっていき、直感的に実装したり使ったりすることが難しくなります。
そんなときはコンポジットパターンが威力を発揮します。
コンポジットパターンは、要素であるオブジェクトと、複数の要素からなる複合オブジェクトを区別なく扱えるという特徴を持ちます。
この特徴を利用することで、構造を再帰的に組み立て、クライアントからの見た目をシンプルに保つことができます。
コンポジットパターンは、ディレクトリ構造のような再帰的な構造を解決することに適しています。
再帰的とは、ディレクトリの下にさらにディレクトリを置くことができるように、自分自身と同じタイプの要素を含むことができる性質のことです。
このような構造では、要素の下にさらに要素を含むことができる複合要素と、末端となる単独要素の2つのタイプの要素から成り立っています。
ディレクトリ構造で言うと、ディレクトリが複合要素、ファイルが単独要素に当たります。
この複合要素と単独要素を組み合わせていくことで再帰的な構造が形成されていきます。
再帰的な構造全体に対する処理を行いたい場合に、複合要素と単独要素を区別して実装してしまうと、処理の実装は非常に煩雑になってしまいますし、新しい要素を加えたいなどの構造に対する変更もコストが大きくなってしまいます。
これに対しコンポジットパターンでは、すべての要素に共通の処理(オペレーション)を持つ抽象クラス(コンポーネント)を定義し、サブクラスとして複合要素(コンポジット)と単独要素(リーフ)を別々に用意しておきます。
コンポジットクラスのオペレーションではリーフクラスのオペレーションをすべて代表して実行するように実装しておくことで、複数の要素の集まりを、まるで1つの要素であるかのように扱えます(図10)。
このように単独要素(リーフクラス)と複合要素(コンポジットクラス)を同一視できるようにしておくと、自分の下位の要素がリーフクラスなのかコンポジットクラスなのかいちいち区別しなくてもよくなります。
再帰的に要素が組み合わさっている複雑な構造の場合でも、親コンポジットに対して処理を実行すれば、あとは再帰構造を伝播していきながらすべてのリーフに対して処理が実行できる仕組みができあがります(図11)。
それでは具体例として、売り上げの集計システムを使ってコンポジットパターンを説明してみましょう(図12)。
このシステムでは、集計処理をどの商品・注文単位でも行うことができるようにします。
図12で言うと、商品1つの値段から、日、月、年などのすべての注文単位で集計可能です。
このようにコンポジットパターンは、任意の要素から階層を辿りすべての要素に処理を実行することを得意としていますので、覚えておきましょう。
このサンプルの場合、末端となる単独要素は「商品」です。
複合要素は商品のまとまりである「注文」と考えてください。
これらに共通する処理は「値段の集計」です。
商品は自分自身の値段しか持たないので「集計」とはちょっと違うと感じてしまうかもしれませんが、1つの商品の値段を集計すると考えてください。
このようにすべてのオブジェクトに共通して「集計」という処理を決めておくことで、再帰的に繋がった全ての商品の集計が容易になります(図13)。
Orderはすべての商品のスーパークラスです。
商品の集まりであるOrderLineと商品であるOrderItemでは値段の集計の仕方が違うので、getAmount()メソッドの実装はそれぞれのクラスに任されます。
Orderを追加するaddOrder()メソッドも持っていますが、ここでは何もしていません(OrderItemクラスに継承されることを考えて、デフォルトの実装ではUnsuportedOperationExceptionというような例外を出すようにしておいてもよいでしょう)。
リスト14 Order.java
public abstract class Order {
public void addOrder(Order order) {
}
public abstract int getAmount();
}
◎OrderLine(リスト15)
OrderLineはOrderの入れ物となるクラスです。
addOrder()メソッドを使ってOrderItemやOrderLineをいくつでも入れることができます。
getAmount()メソッドでは、OrderLineに入っている商品の値段を集計します。
OrderLineの中に入れ子のようにOrderLineが入っている場合でもOrderLine自身が再帰的に集計するので、ぶら下がっているすべての商品の値段が集計されます。
リスト15 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;
}
}
◎OrderItem(リスト16)
OrderItemは末端となる単独要素で、1個の商品を指します。
値段(price)を持っているので、getAmount()メソッドでは自分自身の値段を返します。
リスト16 OrderItem.java
public class OrderItem extends Order {
private int price;
public OrderItem(int price) {
this.price = price;
}
public int getAmount() {
return this.price;
}
}
◎コンポジットパターンはクライアントから見てわかりやすい
すべてのオブジェクトは共通の抽象クラスを持っていますので、クライアントから見て、どれがリーフなのかコンポジットなのかを判別する必要なく、どのオブジェクトも一様に扱うことができます。
また、オブジェクト構造上の、あるリーフオブジェクトをコンポジットオブジェクトに置き換たり、新しいリーフクラスやコンポジットクラスを追加するなど構造に変化がある場合でも、コンポーネントクラスのインタフェースが変わらなければ、クライアントの処理には影響しません。
◎共通インタフェースの重要性
コンポジットパターンでは、どのような視点で構造を抽象化するか、ということが特に重要になります。
このパターンに登場するコンポーネントクラスはすべてのオブジェクトに共通するインタフェースを持つので、あとからインタフェースに変更を加えたくなっても、それは非常に困難になることが予想されます。
あらかじめ構造をきちんとモデル化してあげる必要があるでしょう。
◎出番の多いパターン
アプリケーションを作っていると、コンポジットパターンが適用できそうな再帰的構造を持つデータに出会う機会は非常に多いと言えます。
身近なところではファイルとフォルダの関係がそうですし、XMLで表現されるような構造を持つデータは、コンポジットパターンが適用できる好例です。
膨大な規模のデータ構造を目の当たりにすると、とかく複雑に考えがちですが、構成要素をよく観察し、コンポジットパターンが適用できないか一度考えてみましょう。