🎯 什麼情境該想到我
當你「有一個樹狀結構,而且希望『葉節點』和『容器』能用同一種方式對待」的時候——
檔案系統、UI 元件樹、選單、組織架構、運算式 AST、巢狀的權限群組。
⚙️ 怎麼用
意圖
把物件組合成樹狀結構以表示「部分—整體」的階層。組合模式讓客戶端對「單一物件」和「組合物件」的使用具有一致性。
Component(介面)
▲
├── Leaf(葉:實際做事)
└── Composite(容器:持有一堆 Component)
└──▶ children []Component ★ 遞迴結構
★ 關鍵:Composite 持有的是 Component,而它自己也是 Component —— 所以可以無限巢狀。
★ Go 的長相
type Node interface {
Size() int64
Name() string
}
// ── 葉 ──
type File struct {
name string
size int64
}
func (f File) Size() int64 { return f.size }
func (f File) Name() string { return f.name }
// ── 容器 ──
type Dir struct {
name string
children []Node // ★ 持有 Node,而 Dir 自己也是 Node
}
func (d Dir) Name() string { return d.name }
func (d Dir) Size() int64 {
var total int64
for _, c := range d.children {
total += c.Size() // ★ 遞迴 —— 不需要知道 c 是檔案還是目錄
}
return total
}
// 使用端完全不區分
func report(n Node) {
fmt.Printf("%s: %d bytes\n", n.Name(), n.Size())
}★ 價值就在最後那個
report:它不需要if isDir(...)。
「消掉呼叫端的型別判斷」是組合模式唯一的目的。
★ 「透明」vs「安全」:GoF 沒有給出答案的取捨
問題:Add(child)、Remove(child) 這些方法該放在哪?
| 作法 | 放哪 | 優點 | 缺點 |
|---|---|---|---|
| 透明(transparent) | 放在 Component 介面上,葉節點也要實作 | 客戶端完全不用區分 | ★ 葉節點的 Add 只能回錯誤或 panic——型別系統騙了你 |
| 安全(safe) | 只放在 Composite 上 | ★ 型別安全,葉節點不會有無意義的方法 | 客戶端要做型別斷言才能加子節點 |
Go 的建議做法:安全版 + 小介面
type Node interface{ Size() int64; Name() string } // ★ 只放共通的
type Container interface { // ★ 容器能力另外一個介面
Node
Add(Node)
}
// 需要時才斷言
if c, ok := n.(Container); ok {
c.Add(File{"x.txt", 100})
}★ 這符合 Go 的 「介面小而隱式」(見 工具-Go介面小而隱式)——
不要為了「統一」而讓葉節點背負它做不到的方法。
TypeScript 的長相
type Node =
| { kind: "file"; name: string; size: number }
| { kind: "dir"; name: string; children: Node[] };
// ★ TS 更自然的做法是 discriminated union + 遞迴函式
function size(n: Node): number {
switch (n.kind) {
case "file": return n.size;
case "dir": return n.children.reduce((s, c) => s + size(c), 0);
}
}★ TS 的判別聯集 + 純函式,常常比 class 階層更好用:
真實案例
| 案例 | 葉 / 容器 |
|---|---|
| 檔案系統 | 檔案 / 目錄 |
| DOM、React 元件樹 | 文字節點 / 元素節點 |
| 運算式 AST | 字面量 / 運算子節點 |
fs.WalkDir(Go 1.16+) | ★ 標準庫對檔案樹的遞迴走訪 |
| 巢狀權限群組 | 使用者 / 群組 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 遞迴深度。深樹(或惡意輸入構造的深巢狀 JSON/XML)會爆堆疊。
→ 對不可信輸入一定要設深度上限,或改用顯式堆疊(見 深度優先搜尋)。
這是真實的 DoS 攻擊面。 - ★ 環會造成無限遞迴。「樹」的前提是無環,但如果結構可以被任意組裝(A 包含 B,B 又包含 A),
Size()會無限遞迴直到堆疊爆。→ 加入時檢查,或走訪時記錄 visited。 - 遞迴計算可能很貴。每次
Size()都走全樹 → 大樹上 且重複。
→ 需要頻繁查詢就快取結果並在修改時失效(但那會引入一致性問題)。 - ★ 「過度統一」是常見錯誤。硬把不相似的東西塞進同一個
Component介面,
結果介面變成所有方法的聯集,每個實作都有一半方法回ErrNotSupported。
→ 介面只放真正共通的東西。 - 父指標要不要有:需要往上走(找路徑、冒泡事件)就要
parent,但會產生循環參照
(Go 的 GC 沒問題,但序列化會無限迴圈——記得在 JSON tag 上-)。 - 不需要遞迴結構就別用。只是「一個 list」不是 Composite。