Composite Pattern(組合模式)
是什麼?
Composite Pattern 將物件組合成樹狀結構來表達「部分—整體」的階層關係。它讓客戶端可以用一致的方式處理個別物件(Leaf)和組合物件(Composite),不需要區分兩者。
ℹ️GoF 分類
Composite 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。
什麼時候用?
- 你的資料結構天生就是樹狀的(檔案系統、組織架構、UI 元件樹、菜單)
- 你希望客戶端不用區分「葉節點」和「容器節點」
- 你需要對整棵樹執行遞迴操作(計算總價、渲染全部元件)
什麼時候不該用?
⚠️過度設計警告
如果你的資料結構是平坦的列表而非樹狀,不要強行套 Composite。另外,當 Leaf 和 Composite 的操作差異很大時(例如 Leaf 有但 Composite 沒有的行為),統一介面反而會造成混淆。
執行流程
定義 Component 介面
宣告 Leaf 和 Composite 共用的操作方法
實作 Leaf
Leaf 是樹的末端節點,直接實作操作邏輯
實作 Composite
Composite 持有子節點列表,操作時遞迴呼叫子節點
組裝樹狀結構
將 Leaf 和 Composite 組合成樹
統一操作
Client 對根節點呼叫操作,遞迴傳播到所有子節點
流程解讀:Composite 的核心是「遞迴組合」。Component 介面定義了 Leaf 和 Composite 都要實作的方法(如 GetSize()、Display())。Leaf 直接回傳自身的值,Composite 遍歷 children 並遞迴加總。因為 Composite 的 children 型別是 Component,所以可以放 Leaf 也可以放其他 Composite,形成任意深度的樹。
程式碼範例
C# 版本
// 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 版本
// 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 版本
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 版本
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 只認識 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) |