勉強日記

チラ裏

GoF本 Composite

ねらい

  • オブジェクトを木構造にし、木全体も部分木も葉も一様に扱えるようにする

モチベーション

  • 例えばお絵かきソフト
  • プリミティブな線や図形も、その集合体である複雑な図形も同じように扱いたい
    • 少なくともユーザは同じように扱う
    • コード上も同じように扱いたい
      • さもないと複雑になるから
  • プリミティブなものと集合体とを汎化して、共通の親クラスを設けることで実現

つかいどころ

  • 部分-全体構造(再帰構造のこと?)を扱いたい
  • クライアントコードから、個々のオブジェクトとオブジェクト集合体とを区別しなくて済むようにしたい

登場人物

  • Component
    • 後述のLeaf,Composite共通の基底
    • 子ノードへのアクセス・管理の操作をもつ
    • (optional) 親ノードへのアクセス操作をもつ
    • デフォルト動作を定義する
      • Componentを追加しようとしたら例外を投げる、とか
        • Leafはこれをそのまま使う
        • Compositeはオーバライドする
    • その他、Leaf,Compositeで必要な操作をもつ(和集合)
    • 木構造のノードにあたるので、クラスを強調しない文脈では「ノード」と称する
  • Leaf
    • 葉ノード
  • Composite
    • 中間ノード
    • 子ノードへの参照をもつ
    • Compositeの操作は、子ノードへの処理の委譲
      • +αが前後につくことも
  • Client
    • Componentに依存

クライアントコードからの利用

  • Componentの操作を呼び出す
    • ここにおいてLeafCompositeか、ということを区別しない(できない)

結果

  • プリミティブなオブジェクトから複雑なオブジェクトを構成できる
    • インタフェースは同じ
  • クライアントコードが簡潔になる
  • 新しい種類のComponentを追加するのが簡単になる
    • Componentのインタフェースに変更が加わらなければ、Component派生やClientに変更は生じない
  • デザインが過度に一般化されてしまうこともある
    • 「ある部品には特定の子しか含められない」といった制限を静的型付けでは実現できなくなる
      • 実行時のif分岐等が必要になる

実装にあたり考えるべきこと

  • 親ノードへの参照をもつかどうか
    • メリット
      • ノードの挿入・削除に便利
      • Chaiin of Responsibilityパターンの実装にも便利
    • デメリット
      • ノードオブジェクトの使いまわし・共有に不便
        • 親が複数になり、あいまいさが生じる
      • Flyweightパターンを適用できない
        • 「親への参照」という状態メンバを捨てねばならない
  • Componentのインタフェースが和集合的になること
    • 結果、無意味な操作が生まれる
      • 例えば、Leaf.Addはどう実装する?
  • 子ノードの操作をどこに定義する?
    • 2通り
      1. Composite
        • 型安全
        • 透過性が失われる(ClientにCompositeであることを意識させる)
      2. 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で適切にオーバライドする
  • 子ノードのコレクションは誰が持つ?
    • Componentに持たせると空間のオーバヘッドが生じる
      • Leafが少なければアリかも?
  • 子ノードの順序
    • 例えばお絵かきソフトなら、絵や図形を重ねる順番
    • 構文木なら文そのものに影響してくる
    • Iteratorをつかい、内部構造を隠蔽するとよい
      • 例えばC++std::map<T,U>は赤黒二分木だが、
        そんなこと意識しなくても begin()でキー昇順のイテレータを得られる
  • キャッシングによるパフォーマンスチューニング
    • 例) お絵描きソフト
      • 描画矩形領域がわかっているといろいろうれしい
        • 図形のクリック・ドラッグ判定
        • 不要な描画をスキップできる
      • 複合図形(Composite)の描画矩形領域を計算するうえで、部分木の領域をキャッシュすることで、いちいち全部計算しなくてよくなる
        • 子孫ノードの追加削除があったら再計算が必要になる
          • その再計算も部分的で済む
        • 親ノードへの参照があると、キャッシュが使えなくなったことの連絡に有用
  • 誰がComponentを削除する?
    • GCのない言語では、子ノードの参照をもつ親ノードに責務を負わせるのが普通
    • Flyweightパターンでノードを共用している場合は削除してはいけない
  • 子ノードを保持する内部表現はどうするのがよい?
    • なんでもいい
    • 汎用コレクション
      • 線形リスト
      • 配列
      • ハッシュ
    • 子の個数が決まっている場合は、個別に変数を充ててもいい

関連するパターン

  • Chain of Responsibility
    • 子→親の参照の使途のひとつ
  • Decorator
    • 併用される
      • この場合、DecoratorはComponentのインタフェースを持つ必要がある
  • Flyweight
    • Componentをimmutableにして流用・共用
      • 親ノードへの参照は持てなくなる(mutableだから)
  • Iterator
    • Componentを走査するのに利用
  • Visitor
    • Composite/Leafに散らばるはずの処理を局所化する
      • 例) お絵描きソフト
        • 描画処理は描画クラスDrawerが一元管理するとする
          • DrawerがVisitorにあたる
          • 各図形の描き方を知っていて、描画メソッドを定義する
        • 各図形は、Drawerのどのメソッドで描画されればよいかを知っている
          • 各図形のComponentクラスがAcceptorにあたる

英語

  • without resorting to
    • ~という手段に訴えることなしに
      • 危険なダウンキャストをせずに、という文脈で使用
  • chassis
  • overly
    • 過度に
      • ネガティブなイメージ