1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【デザインパターン】コンポジットパターン解説(Flutter / Android 実例付き)

Posted at

1. パターンの意図

コンポジット(Composite)パターン は、
個々のオブジェクト(Leaf)と、それらをまとめたコンテナ(Composite)を同一視して扱えるようにする デザインパターンです。

解決する問題

  • 階層構造(ツリー構造)を表現したい
  • 「個」と「集合」を 同じ操作で扱いたい
  • ファイルシステム(フォルダとファイル)のように、再帰的な構造を自然に表現したい

ポイント

  • Component:共通インターフェース
  • Leaf:末端要素
  • Composite:子要素を持ち、Component として振る舞う
  • クライアントは「個」と「集合」を区別せず操作できる

2. UML 図


3. Flutter / Dart 実装例

3.1 Component

abstract class FileSystemNode {
  void display(String indent);
}

3.2 Leaf

class FileNode implements FileSystemNode {
  final String name;

  FileNode(this.name);

  @override
  void display(String indent) {
    print("$indent- File: $name");
  }
}

3.3 Composite

class DirectoryNode implements FileSystemNode {
  final String name;
  final List<FileSystemNode> _children = [];

  DirectoryNode(this.name);

  void add(FileSystemNode node) => _children.add(node);
  void remove(FileSystemNode node) => _children.remove(node);

  @override
  void display(String indent) {
    print("$indent+ Directory: $name");
    for (var child in _children) {
      child.display("$indent  ");
    }
  }
}

3.4 利用例

void main() {
  var root = DirectoryNode("root");
  root.add(FileNode("file1.txt"));

  var subDir = DirectoryNode("sub");
  subDir.add(FileNode("file2.txt"));

  root.add(subDir);

  root.display(""); 
}

出力:

+ Directory: root
  - File: file1.txt
  + Directory: sub
    - File: file2.txt

4. Android / Kotlin 実装例

4.1 Component

interface FileSystemNode {
    fun display(indent: String = "")
}

4.2 Leaf

class FileNode(private val name: String) : FileSystemNode {
    override fun display(indent: String) {
        println("$indent- File: $name")
    }
}

4.3 Composite

class DirectoryNode(private val name: String) : FileSystemNode {
    private val children = mutableListOf<FileSystemNode>()

    fun add(node: FileSystemNode) = children.add(node)
    fun remove(node: FileSystemNode) = children.remove(node)

    override fun display(indent: String) {
        println("$indent+ Directory: $name")
        children.forEach { it.display("$indent  ") }
    }
}

4.4 利用例

fun main() {
    val root = DirectoryNode("root")
    root.add(FileNode("file1.txt"))

    val subDir = DirectoryNode("sub")
    subDir.add(FileNode("file2.txt"))

    root.add(subDir)

    root.display()
}

出力は Dart 版と同じ。


5. メリット / デメリット

メリット

  • 個と集合を一貫して扱える(クライアントコードがシンプルになる)
  • 階層構造を自然に表現できる(ファイルシステム、UIツリーなど)
  • 再帰処理との相性が良い

デメリット

  • 設計がやや複雑になる
  • 子要素の管理方法(追加・削除)を誤ると混乱する

6. 実務ユースケース

Flutter

  • Widget ツリーContainer の中に TextRowColumn を入れる構造)
  • メニュー構造(親メニュー・子メニュー)
  • カスタム UI コンポーネントの階層管理

Android (Kotlin)

  • View 階層(ViewGroup と View)
  • ファイルシステムの抽象化
  • ツリー状のデータ構造(メニュー、カテゴリ)

7. 実装上の注意点

  • 子要素の管理 API(add/remove)は Composite のみに持たせるのが一般的
  • Leaf が子管理を持つと SRP 違反になる
  • 再帰構造のため テストでは小さなケースから確認すると安全

8. どんなときに使う?

  • ツリー構造のデータや UI を表現したいとき
  • 個と集合を区別せず処理したいとき
  • Widget / View のように入れ子構造が自然な場面

まとめ

  • コンポジットパターンは「個と集合を同一視して扱う」ためのパターン
  • Flutter では Widget ツリー、Android では View 階層が代表例
  • 再帰的な処理やツリー構造の表現で非常に有効
  • 個と集合を分けずに一貫して操作できるのが最大のメリット

付記

Flutter Widget ツリーの図解(Composite として)

1) ツリー例(Column の中に TextRow、さらに Row の中に IconText

ポイント:

  • Column / RowComposite(子を持つ)
  • Text / IconLeaf(子を持たない)
  • クライアント(フレームワーク)は “Widget” として一様に扱う(個と集合の同一視)

2) 役割マッピング(UML 風)
  • Widget = Component
  • LeafWidget(例:Text, Icon)= Leaf
  • MultiChildWidget(例:Column, Row, Stack)= Composite
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?