Composite
一言でいうと
Composite (コンポジット、 混成) は、 構造に関するデザインパターンの一つで、 オブジェクトからツリー (木) 構造を組み立て、 そのツリー構造がまるで独立したオブジェクトであるかのように扱えるようにします。
![Composite デザインパターン](/images/patterns/content/composite/composite.png?id=73bcf0d94db360b636cd745f710d19db)
問題
Composite パターンは、 アプリの中核となるモデルがツリー構造で表現できる場合にのみ適用する意味があります。
たとえば、 2 種類のオブジェクトがあることを想像してください。 Product
(製品) と Box
(箱) です。 一つの Box
はいくつかの Product
といくつかの小さな Box
を含むことができます。 これらの小さな Box
も、 いくつかの Product
やさらに小さな Box
を含むことができ、 以下同様です。
これらのクラスを使って、 注文システムを作ることにしたとします。 注文は、 包装なしでシンプルな製品かもしれませんし、 箱の中に製品と他の箱が詰め込まれたものかもしれません。 このような注文の合計価格を計算するにはどうすればいいでしょう?
![複雑な注文の例](/images/patterns/diagrams/composite/problem-ja.png?id=6cf90871eeadb5c2ef6a645bf5bd1026)
注文は、 箱に包装された様々な製品と、 それを包む大きな箱かもしれない。 全体の構造は上下逆にした木のようにみえる。
直接的なやり方を試みることは可能です: 箱を全部開けて、 製品を取り出し、 合計を計算。 現実世界では可能なやり方です。 しかしプログラム上では、 単純にループを回すだけではすみません。 Product
と Box
クラスを熟知し、 箱の入れ子の度合いその他のどうでもいい細かいことに気を払う必要があります。 こういう理由で、 直接の解法は、 とてもやりにくかったり、 不可能かもしれません。
解決策
Composite パターンに従うと、 合計価格を計算するためのメソッドが宣言された共通のインターフェースを通して Product
と Box
をアクセスします。
このメソッドの仕組みはどうなっているのでしょうか? 製品の場合は、 単に製品の価格を返すだけです。 箱の場合は、 箱に含まれている各項目ごとにその価格を尋ねてから、 この箱の合計を返します。 項目のいずれかが小さい箱の場合、 その箱もその項目などを調べる、 ということをすべての内容物の価格が計算されるまで繰り返します。 箱は包装手数料のような追加料金を最終的な価格に加えることもできます。
![Composite パターンの提案する解決策](/images/patterns/content/composite/composite-comic-1-ja.png?id=b4bb7878c59ccb8bfad583ad8fbfd637)
Composite パターンを使用して、 ツリー構造のすべての内容物に対して再帰的にある振る舞いを実行できる。
このやり方の最大の利点は、 ツリーを構成するオブジェクトの具象クラスを気にする必要がないことです。 オブジェクトが単純な製品なのか、 豪華な箱なのかを知る必要はありません。 共通のインターフェースですべて同じものを扱うことができます。 メソッドを呼び出すと、 オブジェクト自身がリクエストをツリーの下方に渡します。
現実世界でのたとえ
![軍事組織例](/images/patterns/diagrams/composite/live-example.png?id=548a7cec45b493af66e8bfe524a137d3)
軍事組織の例
ほとんどの国の軍隊は、 階層構造をしています。 軍はいくつかの師団で構成されています。 師団は旅団の集まりで、 旅団は小隊の集合で、 小隊は分隊で構成されています。 最終的に、 分隊は兵士たちで構成された小さい班に分けられます。 命令は、 すべての兵士が何をすべきかを知るまで、 階層の上に与えられ、 各レベルに引き渡されます。
構造
![Composite デザインパターンの構造](/images/patterns/diagrams/composite/structure-ja.png?id=88ff6efa0f4aeb685976bddd178fb927)
![Composite デザインパターンの構造](/images/patterns/diagrams/composite/structure-ja-indexed.png?id=16ad523ec80fff8cc4c215664877405b)
-
コンポーネント (Component) インターフェースは、 ツリーの単純な要素と複雑な要素の両方に共通する操作を記述します。
-
リーフ (Leaf) は、 ツリーの基本要素で、 子要素を持ちません。
リーフのコンポーネントは、 仕事の移譲先がないため、 通常、 実際の作業のほとんどは、 ここで行われることになります。
-
コンテナ (Container、 別名: Composite) は、 子要素を持った要素です。 コンテナはその子要素の具象クラスが何なのかを知らず、 コンポーネント・インターフェースを介してのみ、 子要素とやりとりをします。
リクエストを受け取ると、 コンテナはその作業を子要素に委任し、 中間結果を処理し、 最終結果をクライアントに返します。
-
クライアント (Client) は、 コンポーネントインターフェースを介してすべての要素とやりとりします。 その結果、 クライアントは、 ツリーの単純要素と複雑な要素の両方に対して同じように機能できます。
擬似コード
この例では、 Composite パターンで、 グラフィック・エディターでの幾何学形状の積み重ねの実装をします。
![Composite 例の構造](/images/patterns/diagrams/composite/example.png?id=98ba81d07c979038dd2ebf3c83a2e19f)
幾何学形状エディターの例
CompoundGraphic
(複合グラフィック) クラスは、 任意の数の子形状を含むコンテナです。 子形状は、 複合形状かもしれません。 複合形状は、 単純形状と同じメソッドを持っています。 しかし、 複合形状は、 自分自身で何かを行うのではなく、 リクエストをすべての子要素に再帰的に渡し、 結果を 「集計」 します。
クライアントは、 すべての形状クラスに共通の単一のインターフェースを通じて、 すべての形状とやりとりします。 したがって、 クライアントは、 相手が単純形状なのか複合形状なのかを知りません。 クライアントは、 具体的なクラスと密に結合することなく、 非常に複雑なオブジェクト構造を扱うことができます。
// コンポーネント・インターフェースは、コンポジット中の単純なオブジェクト
// と複雑なオブジェクトの両方に共通する操作を宣言。
interface Graphic is
method move(x, y)
method draw()
// リーフ・クラスは、合成の末端のオブジェクトを表現する。リーフのオブジェ
// クトは、副オブジェクトを持てない。通常、本当の仕事をするのは、リーフ・
// オブジェクトで、コンポジット・オブジェクトは仕事を副オブジェクトに委任。
class Dot implements Graphic is
field x, y
constructor Dot(x, y) { …… }
method move(x, y) is
this.x += x, this.y += y
method draw() is
// X と Y に点を描画。
// すべてのコンポーネントクラスは他のコンポーネントを拡張可能。
class Circle extends Dot is
field radius
constructor Circle(x, y, radius) { …… }
method draw() is
// X と Y に半径 R の円を描画。
// コンポジット・クラスは子を持つかもしれない複雑なコンポーネントを表現す
// る。コンポジット・オブジェクトは通常実際の仕事を子に委任し、結果を「ま
// とめあげる」。
class CompoundGraphic implements Graphic is
field children: array of Graphic
// コンポジット・オブジェクトは、他のコンポーネント(単純、複雑どちら
// の種類でも)を子リストに追加も削除もできる。
method add(child: Graphic) is
// 子配列に子を一つ追加。
method remove(child: Graphic) is
// 子配列から子を一つ削除。
method move(x, y) is
foreach (child in children) do
child.move(x, y)
// コンポジットは、特定の方法でその主要なロジックを実行。すべての子た
// ちを再帰的に探索し、結果を収集し、まとめあげる。コンポジットの子要
// 素が自身の子要素にこれらの呼び出しを行い、それがまたその子要素に、
// と続くため、結果としてオブジェクト・ツリー全体が探索される。
method draw() is
// 1. 各子コンポーネントに対して:
// - コンポーネントを描画。
// - 境界長方形を更新。
// 2. 境界座標を使用して破線の長方形を描画。
// クライアント・コードは、基底インターフェースを介してすべてのコンポーネ
// ントと動作。このようにしてクライアント・コードは単純なリーフ・コンポー
// ネントと複雑なコンポジットの両方をサポート可能。
class ImageEditor is
field all: CompoundGraphic
method load() is
all = new CompoundGraphic()
all.add(new Dot(1, 2))
all.add(new Circle(5, 3, 10))
// ……
// 選択したコンポーネントを組み合わせて、一つの複雑なコンポジット・コ
// ンポーネントにする。
method groupSelected(components: array of Graphic) is
group = new CompoundGraphic()
foreach (component in components) do
group.add(component)
all.remove(component)
all.add(group)
// 全コンポーネントが描画される。
all.draw()
適応性
ツリーのようなオブジェクト構造を実装する場合は、 Composite パターンを使用します。
Composite パターンは、 単純なリーフと複雑なコンテナという、 共通のインターフェースを持った二つの基本的な要素型からなっています。 コンテナは、 リーフと他のコンテナの両方から成り立っています。 これにより、 木に似た入れ子になった再帰オブジェクト構造を構築できます。
クライアント・コードが、 単純な要素と複雑な要素を同等に扱えるようにするために、 このパターンを適用してください。
Composite パターンで定義されたすべての要素は、 共通のインターフェースを共有します。 このインターフェースを使用することにより、 クライアントは扱うオブジェクトの具体的なクラスが何なのかを心配する必要がなくなります。
実装方法
-
アプリの中核のモデルが、 ツリー構造で表現可能なことをまず確認してください。 単純項目とコンテナに分けてみてください。 コンテナは、 単純要素と他のコンテナを含むことができる、 ということをお忘れなく。
-
単純要素と複合要素の両方に意味のあるメソッドを並べたコンポーネント・インターフェースを宣言してください。
-
単純要素を表現する、 リーフ・クラスを作成します。 一つのプログラムには、 複数のリーフ・クラスがあるかもしれません。
-
複雑な要素を表現する、 コンテナ・クラスを作成します。 このクラスには、 子要素への参照をしまっておくための配列フィールドを設けます。 配列は、 リーフとコンテナの両方をしまうことが可能である必要があります。 そのため、 コンポーネント・インターフェースを使って宣言してください。
コンポーネント・インターフェースのメソッド実装の際には、 コンテナがほとんどの作業を子要素に委任するべきだということをお忘れなく。
-
最後に、 コンテナに子要素を追加したり削除するためのメソッドを定義してください。
これらの操作は、 コンポーネント・インターフェースで宣言することもできる、 ということを心にとめておいてください。 これは、 リーフ・クラスではこれらのメソッドは空になるため、 インターフェース分離の原則に違反します。 しかし、 そうすることで、 ツリーの作成時に、 クライアントは全要素を同等に扱えます。
長所と短所
- 複雑なツリー構造をより便利に扱うことができます。 多相性と再帰を活用。
- 開放閉鎖の原則。 既存のコードを壊すことなく、 オブジェクト・ツリーと動作可能な新規の要素型をアプリに導入可能。
- 機能が大きく異なるクラスの共通インターフェース作成は困難である可能性。 特定の状況下では、 コンポーネント・インターフェースの過度な一般化が必要で、 理解困難。
他のパターンとの関係
-
Builder は、 複雑な Composite ツリー作成に使用できます。 構築ステップを再帰的に行なうように プログラムします。
-
Chain of Responsibility は、 よく Composite と一緒に使われます。 この場合、 リーフ (末端) のコンポーネントがリクエストを受ける時、 リクエストは、 全部の親コンポーネントからオブジェクト・ツリーのルート (根) までを通るかもしれません。
-
RAM を節約するために、 Composite ツリーの共有リーフ・ノードを Flyweights として実装できます。
-
Composite と Decorator は両方とも、 任意の数のオブジェクトを組織するために再起的合成を使用するので、 似たような構造図をしています。
Decorator は、 Composite に似ていますが、 子コンポーネントは一つしかありません。 もう一つの大きな違いは、 Decorator は内包するオブジェクトに責任を追加するのに対し、 Composite は、 単にその子たちの結果を 「まとめあげる」 だけです。
しかしながら、 これらのパターンは互いに協力できます。 Decorator を使用して、 Composite ツリー内の特定のオブジェクトの振る舞いを拡張できます。
-
Composite と Decorator を多用する設計に対しては、 Prototype の使用が有益かもしれません。 このパターンを適用すると、 複雑な構造を初めから再構築するのではなく、 それをクローンします。