Composite Pattern(組合模式)

是什麼?

Composite Pattern 將物件組合成樹狀結構來表達「部分—整體」的階層關係。它讓客戶端可以用一致的方式處理個別物件(Leaf)和組合物件(Composite),不需要區分兩者。

ℹ️GoF 分類

Composite 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。

什麼時候用?

什麼時候不該用?

⚠️過度設計警告

如果你的資料結構是平坦的列表而非樹狀,不要強行套 Composite。另外,當 Leaf 和 Composite 的操作差異很大時(例如 Leaf 有但 Composite 沒有的行為),統一介面反而會造成混淆。

執行流程

1

定義 Component 介面

宣告 Leaf 和 Composite 共用的操作方法

2

實作 Leaf

Leaf 是樹的末端節點,直接實作操作邏輯

3

實作 Composite

Composite 持有子節點列表,操作時遞迴呼叫子節點

4

組裝樹狀結構

將 Leaf 和 Composite 組合成樹

5

統一操作

Client 對根節點呼叫操作,遞迴傳播到所有子節點

流程解讀:Composite 的核心是「遞迴組合」。Component 介面定義了 Leaf 和 Composite 都要實作的方法(如 GetSize()Display())。Leaf 直接回傳自身的值,Composite 遍歷 children 並遞迴加總。因為 Composite 的 children 型別是 Component,所以可以放 Leaf 也可以放其他 Composite,形成任意深度的樹。

程式碼範例

C# 版本

csharp
// Component 介面
public interface IFileSystemItem
{
    string Name { get; }
    long GetSize();
    string Display(int indent = 0);
}
 
// Leaf:檔案
public class File : IFileSystemItem
{
    public string Name { get; }
    private readonly long _size;
 
    public File(string name, long size) { Name = name; _size = size; }
 
    public long GetSize() => _size;
 
    public string Display(int indent = 0)
        => $"{new string(' ', indent)}{Name} ({_size} bytes)";
}
 
// Composite:資料夾
public class Folder : IFileSystemItem
{
    public string Name { get; }
    private readonly List<IFileSystemItem> _children = new();
 
    public Folder(string name) { Name = name; }
 
    public void Add(IFileSystemItem item) => _children.Add(item);
 
    public long GetSize() => _children.Sum(c => c.GetSize());
 
    public string Display(int indent = 0)
    {
        var result = $"{new string(' ', indent)}[{Name}]";
        foreach (var child in _children)
            result += "\n" + child.Display(indent + 2);
        return result;
    }
}
 
// 使用
var root = new Folder("src");
var docs = new Folder("docs");
docs.Add(new File("readme.md", 1200));
docs.Add(new File("guide.md", 3400));
root.Add(docs);
root.Add(new File("index.ts", 500));
 
Console.WriteLine(root.Display());
Console.WriteLine($"Total size: {root.GetSize()} bytes");

TypeScript 版本

typescript
// Component 介面
interface FileSystemItem {
  name: string;
  getSize(): number;
  display(indent?: number): string;
}
 
// Leaf
class File implements FileSystemItem {
  constructor(public name: string, private size: number) {}
 
  getSize(): number {
    return this.size;
  }
 
  display(indent = 0): string {
    return `${" ".repeat(indent)}${this.name} (${this.size} bytes)`;
  }
}
 
// Composite
class Folder implements FileSystemItem {
  private children: FileSystemItem[] = [];
 
  constructor(public name: string) {}
 
  add(item: FileSystemItem): void {
    this.children.push(item);
  }
 
  getSize(): number {
    return this.children.reduce((sum, child) => sum + child.getSize(), 0);
  }
 
  display(indent = 0): string {
    const lines = [`${" ".repeat(indent)}[${this.name}]`];
    this.children.forEach((child) => lines.push(child.display(indent + 2)));
    return lines.join("\n");
  }
}
 
// 使用
const root = new Folder("src");
const docs = new Folder("docs");
docs.add(new File("readme.md", 1200));
docs.add(new File("guide.md", 3400));
root.add(docs);
root.add(new File("index.ts", 500));
 
console.log(root.display());
console.log(`Total size: ${root.getSize()} bytes`);

Python 版本

python
from abc import ABC, abstractmethod
 
# Component 介面
class FileSystemItem(ABC):
    @abstractmethod
    def get_size(self) -> int:
        pass
 
    @abstractmethod
    def display(self, indent: int = 0) -> str:
        pass
 
# Leaf
class File(FileSystemItem):
    def __init__(self, name: str, size: int):
        self.name = name
        self._size = size
 
    def get_size(self) -> int:
        return self._size
 
    def display(self, indent: int = 0) -> str:
        return f"{' ' * indent}{self.name} ({self._size} bytes)"
 
# Composite
class Folder(FileSystemItem):
    def __init__(self, name: str):
        self.name = name
        self._children: list[FileSystemItem] = []
 
    def add(self, item: FileSystemItem) -> None:
        self._children.append(item)
 
    def get_size(self) -> int:
        return sum(child.get_size() for child in self._children)
 
    def display(self, indent: int = 0) -> str:
        lines = [f"{' ' * indent}[{self.name}]"]
        for child in self._children:
            lines.append(child.display(indent + 2))
        return "\n".join(lines)
 
# 使用
root = Folder("src")
docs = Folder("docs")
docs.add(File("readme.md", 1200))
docs.add(File("guide.md", 3400))
root.add(docs)
root.add(File("index.ts", 500))
 
print(root.display())
print(f"Total size: {root.get_size()} bytes")

Java 版本

java
import java.util.ArrayList;
import java.util.List;
 
// Component 介面
public interface FileSystemItem {
    String getName();
    long getSize();
    String display(int indent);
}
 
// Leaf
public class File implements FileSystemItem {
    private String name;
    private long size;
 
    public File(String name, long size) { this.name = name; this.size = size; }
 
    public String getName() { return name; }
    public long getSize() { return size; }
 
    public String display(int indent) {
        return " ".repeat(indent) + name + " (" + size + " bytes)";
    }
}
 
// Composite
public class Folder implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();
 
    public Folder(String name) { this.name = name; }
 
    public String getName() { return name; }
 
    public void add(FileSystemItem item) { children.add(item); }
 
    public long getSize() {
        return children.stream().mapToLong(FileSystemItem::getSize).sum();
    }
 
    public String display(int indent) {
        StringBuilder sb = new StringBuilder(" ".repeat(indent) + "[" + name + "]");
        for (FileSystemItem child : children) {
            sb.append("\n").append(child.display(indent + 2));
        }
        return sb.toString();
    }
}
 
// 使用
Folder root = new Folder("src");
Folder docs = new Folder("docs");
docs.add(new File("readme.md", 1200));
docs.add(new File("guide.md", 3400));
root.add(docs);
root.add(new File("index.ts", 500));
 
System.out.println(root.display(0));
System.out.println("Total size: " + root.getSize() + " bytes");

結構圖

Client
uses
Component (FileSystemItem)
Leaf (File)
implements
Composite (Folder)

結構解讀:Client 只認識 Component 介面,不區分 Leaf 和 Composite。Leaf(File)直接實作操作邏輯,Composite(Folder)持有子節點列表並在操作時遞迴委派。關鍵是 Composite 的 children 型別也是 Component,所以子節點可以是 Leaf 也可以是另一個 Composite,形成任意深度的樹狀結構。

實戰補充

💡資深開發者經驗

Composite 在前端框架中無處不在 — React 的元件樹就是典型的 Composite 結構。每個元件可以包含子元件,渲染時從根節點遞迴往下。在後端,組織架構(部門包含子部門和員工)、權限系統(權限群組包含子群組和個別權限)、電商的商品分類都是 Composite 的經典應用。注意要小心循環引用:如果 A 資料夾包含 B,B 又包含 A,遞迴操作會無限迴圈。

理解測驗

🤔 Composite Pattern 的核心目的是什麼?

🤔 以下哪個不是 Composite Pattern 的適用場景?

🤔 在 Composite Pattern 中,Composite 節點的 getSize() 是怎麼運作的?

面試常見問題

Q: Composite Pattern 中,add()/remove() 方法該放在 Component 介面還是只放在 Composite?

A: 這是經典的「透明性 vs 安全性」取捨。放在 Component(透明方式):Client 不需要區分 Leaf 和 Composite,但 Leaf 的 add() 要拋例外或空操作。只放在 Composite(安全方式):Leaf 不會有不合理的方法,但 Client 需要型別檢查。實務上多數框架選擇安全方式。

Q: 如何防止 Composite 的循環引用?

A: 在 add() 方法中檢查:(1) 不能把自己加為子節點;(2) 遞迴檢查子樹中是否已包含要加入的節點。或者在設計時限制樹的深度。

相關模式

| 模式 | 關係 | |------|------| | Iterator | 常搭配使用 — Iterator 提供統一的方式走訪 Composite 的樹狀結構(DFS、BFS) | | Visitor | 常搭配使用 — Visitor 對 Composite 結構中的每種節點型別執行不同的操作 | | Decorator | 都使用遞迴組合,但 Decorator 是線性鏈,Composite 是樹狀結構 | | Chain of Responsibility | Composite 的父子關係可以用來建立責任鏈 — 子節點處理不了就往父節點傳 |

重點整理

💡一句話記住

Composite Pattern = 樹狀遞迴:一個和一群用同一個介面操作。 口訣:「葉子和資料夾一視同仁」

| 概念 | 說明 | |------|------| | Component(共用介面) | Leaf 和 Composite 共同實作的介面 | | Leaf(葉節點) | 樹的末端,直接執行操作 | | Composite(組合節點) | 持有子節點,操作時遞迴委派 | | 核心好處 | Client 不用區分個體和集合,統一操作 | | 代價 | 統一介面可能讓 Leaf 被迫實作不合理的方法(如 add) |

你可能也想看

Bridge PatternDecorator Pattern

按 ← → 鍵切換課程