GoF本 Composite
ねらい
- オブジェクトを木構造にし、木全体も部分木も葉も一様に扱えるようにする
モチベーション
- 例えばお絵かきソフト
- プリミティブな線や図形も、その集合体である複雑な図形も同じように扱いたい
- 少なくともユーザは同じように扱う
- コード上も同じように扱いたい
- さもないと複雑になるから
- プリミティブなものと集合体とを汎化して、共通の親クラスを設けることで実現
つかいどころ
- 部分-全体構造(再帰構造のこと?)を扱いたい
- クライアントコードから、個々のオブジェクトとオブジェクト集合体とを区別しなくて済むようにしたい
登場人物
Component
- 後述の
Leaf
,Composite
共通の基底 - 子ノードへのアクセス・管理の操作をもつ
- (optional) 親ノードへのアクセス操作をもつ
- デフォルト動作を定義する
Component
を追加しようとしたら例外を投げる、とかLeaf
はこれをそのまま使うComposite
はオーバライドする
- その他、
Leaf
,Composite
で必要な操作をもつ(和集合) - 木構造のノードにあたるので、クラスを強調しない文脈では「ノード」と称する
- 後述の
Leaf
- 葉ノード
Composite
- 中間ノード
- 子ノードへの参照をもつ
Composite
の操作は、子ノードへの処理の委譲- +αが前後につくことも
- Client
Component
に依存
クライアントコードからの利用
Component
の操作を呼び出す- ここにおいて
Leaf
かComposite
か、ということを区別しない(できない)
- ここにおいて
結果
- プリミティブなオブジェクトから複雑なオブジェクトを構成できる
- インタフェースは同じ
- クライアントコードが簡潔になる
- 新しい種類の
Component
を追加するのが簡単になるComponent
のインタフェースに変更が加わらなければ、Component
派生やClientに変更は生じない
- デザインが過度に一般化されてしまうこともある
- 「ある部品には特定の子しか含められない」といった制限を静的型付けでは実現できなくなる
- 実行時のif分岐等が必要になる
- 「ある部品には特定の子しか含められない」といった制限を静的型付けでは実現できなくなる
実装にあたり考えるべきこと
- 親ノードへの参照をもつかどうか
- メリット
- ノードの挿入・削除に便利
- Chaiin of Responsibilityパターンの実装にも便利
- デメリット
- ノードオブジェクトの使いまわし・共有に不便
- 親が複数になり、あいまいさが生じる
- Flyweightパターンを適用できない
- 「親への参照」という状態メンバを捨てねばならない
- ノードオブジェクトの使いまわし・共有に不便
- メリット
Component
のインタフェースが和集合的になること- 結果、無意味な操作が生まれる
- 例えば、
Leaf.Add
はどう実装する?
- 例えば、
- 結果、無意味な操作が生まれる
- 子ノードの操作をどこに定義する?
- 2通り
-
Composite
- 型安全
- 透過性が失われる(Clientに
Composite
であることを意識させる)
-
Component
- 型安全でなくなる
Leaf
のAddを呼び出してしまったら何が起きる?
- 透過性が保たれる(
Composite
パターンの導入のそもそもの動機である)
- 型安全でなくなる
-
- 1.で、
Component
からComposite
への危険なダウンキャストを避けるにはComponent.GetComposite()
を定義、デフォルトでnullptr
を返すようにするLeaf
:デフォルト動作を使用するComposite
:return this;
でオーバライド
- nullチェックが必要だが
Component
からComposite
を得られる - 透過性は結局確保できない(nullチェックが必要な時点で)
- 透過性を確保するには、結局2.しかない
Leaf.Add/Remove
とかどうすんのRemove
の意味を変える(イマイチ)Leaf.Remove
は「自身を親から削除する」ことにする- かくして一貫性が失われたのであった
- しかも
Add
の問題は解決しない
Component.Add/Remove
はデフォルトで失敗するようにする- 例外を投げるとか
Composite
で適切にオーバライドする
- 2通り
- 子ノードのコレクションは誰が持つ?
Component
に持たせると空間のオーバヘッドが生じるLeaf
が少なければアリかも?
- 子ノードの順序
- キャッシングによるパフォーマンスチューニング
- 例) お絵描きソフト
- 描画矩形領域がわかっているといろいろうれしい
- 図形のクリック・ドラッグ判定
- 不要な描画をスキップできる
- 複合図形(
Composite
)の描画矩形領域を計算するうえで、部分木の領域をキャッシュすることで、いちいち全部計算しなくてよくなる- 子孫ノードの追加削除があったら再計算が必要になる
- その再計算も部分的で済む
- 親ノードへの参照があると、キャッシュが使えなくなったことの連絡に有用
- 子孫ノードの追加削除があったら再計算が必要になる
- 描画矩形領域がわかっているといろいろうれしい
- 例) お絵描きソフト
- 誰が
Component
を削除する? - 子ノードを保持する内部表現はどうするのがよい?
関連するパターン
- Chain of Responsibility
- 子→親の参照の使途のひとつ
- Decorator
- 併用される
- この場合、Decoratorは
Component
のインタフェースを持つ必要がある
- この場合、Decoratorは
- 併用される
- Flyweight
Component
をimmutableにして流用・共用- 親ノードへの参照は持てなくなる(mutableだから)
- Iterator
Component
を走査するのに利用
- Visitor
- 各
Composite
/Leaf
に散らばるはずの処理を局所化する- 例) お絵描きソフト
- 描画処理は描画クラス
Drawer
が一元管理するとするDrawer
がVisitorにあたる- 各図形の描き方を知っていて、描画メソッドを定義する
- 各図形は、
Drawer
のどのメソッドで描画されればよいかを知っている- 各図形の
Component
クラスがAcceptorにあたる
- 各図形の
- 描画処理は描画クラス
- 例) お絵描きソフト
- 各