Composite
一言でいうと
Composite (コンポジット、 混成) は、 構造に関するデザインパターンの一つで、 オブジェクトからツリー (木) 構造を組み立て、 そのツリー構造がまるで独立したオブジェクトであるかのように扱えるようにします。
問題
Composite パターンは、 アプリの中核となるモデルがツリー構造で表現できる場合にのみ適用する意味があります。
たとえば、 2 種類のオブジェクトがあることを想像してください。 Product
(製品) と Box
(箱) です。 一つの Box
はいくつかの Product
といくつかの小さな Box
を含むことができます。 これらの小さな Box
も、 いくつかの Product
やさらに小さな Box
を含むことができ、 以下同様です。
これらのクラスを使って、 注文システムを作ることにしたとします。 注文は、 包装なしでシンプルな製品かもしれませんし、 箱の中に製品と他の箱が詰め込まれたものかもしれません。 このような注文の合計価格を計算するにはどうすればいいでしょう?
直接的なやり方を試みることは可能です: 箱を全部開けて、 製品を取り出し、 合計を計算。 現実世界では可能なやり方です。 しかしプログラム上では、 単純にループを回すだけではすみません。 Product
と Box
クラスを熟知し、 箱の入れ子の度合いその他のどうでもいい細かいことに気を払う必要があります。 こういう理由で、 直接の解法は、 とてもやりにくかったり、 不可能かもしれません。
解決策
Composite パターンに従うと、 合計価格を計算するためのメソッドが宣言された共通のインターフェースを通して Product
と Box
をアクセスします。
このメソッドの仕組みはどうなっているのでしょうか? 製品の場合は、 単に製品の価格を返すだけです。 箱の場合は、 箱に含まれている各項目ごとにその価格を尋ねてから、 この箱の合計を返します。 項目のいずれかが小さい箱の場合、 その箱もその項目などを調べる、 ということをすべての内容物の価格が計算されるまで繰り返します。 箱は包装手数料のような追加料金を最終的な価格に加えることもできます。
このやり方の最大の利点は、 ツリーを構成するオブジェクトの具象クラスを気にする必要がないことです。 オブジェクトが単純な製品なのか、 豪華な箱なのかを知る必要はありません。 共通のインターフェースですべて同じものを扱うことができます。 メソッドを呼び出すと、 オブジェクト自身がリクエストをツリーの下方に渡します。
現実世界でのたとえ
ほとんどの国の軍隊は、 階層構造をしています。 軍はいくつかの師団で構成されています。 師団は旅団の集まりで、 旅団は小隊の集合で、 小隊は分隊で構成されています。 最終的に、 分隊は兵士たちで構成された小さい班に分けられます。 命令は、 すべての兵士が何をすべきかを知るまで、 階層の上に与えられ、 各レベルに引き渡されます。
構造
-
コンポーネント (Component) インターフェースは、 ツリーの単純な要素と複雑な要素の両方に共通する操作を記述します。
-
リーフ (Leaf) は、 ツリーの基本要素で、 子要素を持ちません。
リーフのコンポーネントは、 仕事の移譲先がないため、 通常、 実際の作業のほとんどは、 ここで行われることになります。
-
コンテナ (Container、 別名: Composite) は、 子要素を持った要素です。 コンテナはその子要素の具象クラスが何なのかを知らず、 コンポーネント・インターフェースを介してのみ、 子要素とやりとりをします。
リクエストを受け取ると、 コンテナはその作業を子要素に委任し、 中間結果を処理し、 最終結果をクライアントに返します。
-
クライアント (Client) は、 コンポーネントインターフェースを介してすべての要素とやりとりします。 その結果、 クライアントは、 ツリーの単純要素と複雑な要素の両方に対して同じように機能できます。
擬似コード
この例では、 Composite パターンで、 グラフィック・エディターでの幾何学形状の積み重ねの実装をします。
CompoundGraphic
(複合グラフィック) クラスは、 任意の数の子形状を含むコンテナです。 子形状は、 複合形状かもしれません。 複合形状は、 単純形状と同じメソッドを持っています。 しかし、 複合形状は、 自分自身で何かを行うのではなく、 リクエストをすべての子要素に再帰的に渡し、 結果を 「集計」 します。
クライアントは、 すべての形状クラスに共通の単一のインターフェースを通じて、 すべての形状とやりとりします。 したがって、 クライアントは、 相手が単純形状なのか複合形状なのかを知りません。 クライアントは、 具体的なクラスと密に結合することなく、 非常に複雑なオブジェクト構造を扱うことができます。
適応性
ツリーのようなオブジェクト構造を実装する場合は、 Composite パターンを使用します。
Composite パターンは、 単純なリーフと複雑なコンテナという、 共通のインターフェースを持った二つの基本的な要素型からなっています。 コンテナは、 リーフと他のコンテナの両方から成り立っています。 これにより、 木に似た入れ子になった再帰オブジェクト構造を構築できます。
クライアント・コードが、 単純な要素と複雑な要素を同等に扱えるようにするために、 このパターンを適用してください。
Composite パターンで定義されたすべての要素は、 共通のインターフェースを共有します。 このインターフェースを使用することにより、 クライアントは扱うオブジェクトの具体的なクラスが何なのかを心配する必要がなくなります。
実装方法
-
アプリの中核のモデルが、 ツリー構造で表現可能なことをまず確認してください。 単純項目とコンテナに分けてみてください。 コンテナは、 単純要素と他のコンテナを含むことができる、 ということをお忘れなく。
-
単純要素と複合要素の両方に意味のあるメソッドを並べたコンポーネント・インターフェースを宣言してください。
-
単純要素を表現する、 リーフ・クラスを作成します。 一つのプログラムには、 複数のリーフ・クラスがあるかもしれません。
-
複雑な要素を表現する、 コンテナ・クラスを作成します。 このクラスには、 子要素への参照をしまっておくための配列フィールドを設けます。 配列は、 リーフとコンテナの両方をしまうことが可能である必要があります。 そのため、 コンポーネント・インターフェースを使って宣言してください。
コンポーネント・インターフェースのメソッド実装の際には、 コンテナがほとんどの作業を子要素に委任するべきだということをお忘れなく。
-
最後に、 コンテナに子要素を追加したり削除するためのメソッドを定義してください。
これらの操作は、 コンポーネント・インターフェースで宣言することもできる、 ということを心にとめておいてください。 これは、 リーフ・クラスではこれらのメソッドは空になるため、 インターフェース分離の原則に違反します。 しかし、 そうすることで、 ツリーの作成時に、 クライアントは全要素を同等に扱えます。
長所と短所
- 複雑なツリー構造をより便利に扱うことができます。 多相性と再帰を活用。
- 開放閉鎖の原則。 既存のコードを壊すことなく、 オブジェクト・ツリーと動作可能な新規の要素型をアプリに導入可能。
- 機能が大きく異なるクラスの共通インターフェース作成は困難である可能性。 特定の状況下では、 コンポーネント・インターフェースの過度な一般化が必要で、 理解困難。
他のパターンとの関係
-
Builder は、 複雑な Composite ツリー作成に使用できます。 構築ステップを再帰的に行なうように プログラムします。
-
Chain of Responsibility は、 よく Composite と一緒に使われます。 この場合、 リーフ (末端) のコンポーネントがリクエストを受ける時、 リクエストは、 全部の親コンポーネントからオブジェクト・ツリーのルート (根) までを通るかもしれません。
-
RAM を節約するために、 Composite ツリーの共有リーフ・ノードを Flyweights として実装できます。
-
Composite と Decorator は両方とも、 任意の数のオブジェクトを組織するために再起的合成を使用するので、 似たような構造図をしています。
Decorator は、 Composite に似ていますが、 子コンポーネントは一つしかありません。 もう一つの大きな違いは、 Decorator は内包するオブジェクトに責任を追加するのに対し、 Composite は、 単にその子たちの結果を 「まとめあげる」 だけです。
しかしながら、 これらのパターンは互いに協力できます。 Decorator を使用して、 Composite ツリー内の特定のオブジェクトの振る舞いを拡張できます。
-
Composite と Decorator を多用する設計に対しては、 Prototype の使用が有益かもしれません。 このパターンを適用すると、 複雑な構造を初めから再構築するのではなく、 それをクローンします。