在軟體開發中,樹狀結構無處不在——從檔案系統的目錄架構、UI 控制項的巢狀關係,到企業組織的部門層級。如何優雅地處理「部分-整體」這類層次結構,讓客戶端程式碼無需區分單一物件與複合物件,便是組合模式(Composite Pattern)要解決的核心問題。本文將透過繪圖系統的實戰案例,深入剖析組合模式的兩種實現風格,並探討其在 .NET 框架中的經典應用。
組合模式的意圖非常明確:將物件組合成樹形結構以表示「部分-整體」的層次結構,使得用戶對單個物件和組合物件的使用具有一致性(GoF《設計模式》)。
當你遇到以下情況時,應優先考慮組合模式:
算術表達式是組合模式的教科書級範例。表達式可以是單一運算元(如數字 2),也可以是運算元與運算子的組合(如 2 + 3),甚至是巢狀表達式(如 (2+3) × (4×6))。這裡的關鍵在於:無論是簡單的數字還是複雜的子表達式,都被視為同一類型的「運算元」,從而實現了遞迴處理的統一性。

假設我們要開發一個繪圖系統,包含基本圖元(直線、圓形、矩形)與複合圖元(由基本圖元組成的圖片)。初始設計中,Picture 類別僅提供 GetChilds() 方法回傳子物件集合,客戶端程式碼必須判斷物件型別並手動遞迴:
// 客戶端程式碼片段
if (graphic is Picture) {
foreach (var child in ((Picture)graphic).GetChilds()) {
// 遞迴處理...
}
} else {
graphic.Draw();
}
這種做法將客戶端與複合物件的內部結構強耦合,違反了「依賴倒置原則」,且每新增一種複合圖元都需修改客戶端邏輯。
解決方案是將子物件管理方法(Add、Remove)提升至抽象基類 Graphics,使葉子節點與複合節點擁有完全一致的介面:
public abstract class Graphics {
protected string _name;
public Graphics(string name) { this._name = name; }
public abstract void Draw();
public abstract void Add(Graphics g);
public abstract void Remove(Graphics g);
}
public class Picture : Graphics {
private ArrayList _children = new ArrayList();
public Picture(string name) : base(name) { }
public override void Draw() {
Console.WriteLine("Draw " + _name);
foreach (Graphics g in _children) g.Draw(); // 遞迴委派
}
public override void Add(Graphics g) => _children.Add(g);
public override void Remove(Graphics g) => _children.Remove(g);
}
// 葉子節點:拋出例外或空實作
public class Line : Graphics {
public Line(string name) : base(name) { }
public override void Draw() => Console.WriteLine("Draw " + _name);
public override void Add(Graphics g) => throw new NotSupportedException();
public override void Remove(Graphics g) => throw new NotSupportedException();
}

優點:客戶端完全面向抽象介面編程,無需區分葉子與複合節點。
缺點:葉子節點被迫實作無意義的 Add/Remove,錯誤延遲到執行期才被發現。
若優先考慮型別安全,可將管理方法僅定義在 Picture 類別中,葉子節點保持精簡:
public abstract class Graphics {
protected string _name;
public Graphics(string name) { this._name = name; }
public abstract void Draw(); // 僅保留核心操作
}
public class Picture : Graphics {
private ArrayList _children = new ArrayList();
public Picture(string name) : base(name) { }
public override void Draw() {
Console.WriteLine("Draw " + _name);
foreach (Graphics g in _children) g.Draw();
}
// 管理方法僅在複合類別中定義
public void Add(Graphics g) => _children.Add(g);
public void Remove(Graphics g) => _children.Remove(g);
}

權衡:犧牲了介面的完全透明性,換取編譯期的型別安全。選擇哪種風格,取決於專案對「安全性」與「透明性」的偏好。
引入組合模式後,客戶端程式碼變得驚人簡潔:
public class App {
public static void Main() {
Picture root = new Picture("Root");
root.Add(new Line("Line"));
root.Add(new Circle("Circle"));
root.Add(new Rectangle("Rectangle"));
root.Draw(); // 一行程式碼搞定整棵樹的繪製
}
}
無需判斷型別、無需手寫遞迴——複雜性被封裝在複合物件內部,客戶端只需呼叫統一的 Draw()。
在 Enterprise Library 2.0 的 ObjectBuilder 組件中,「定位器」採用了標準的組合模式:
IReadableLocator 介面,定義查詢、遍歷、父節點導航等操作Locator 類別,實作 IReadWriteLocator,管理子定位器鏈表ReadOnlyLocator,封裝內部定位器並屏蔽寫入操作
關鍵程式碼片段展示了遞迴查找邏輯:
public IReadableLocator FindBy(SearchMode options, Predicate<KeyValuePair<object, object>> predicate) {
Locator results = new Locator();
IReadableLocator currentLocator = this;
while (currentLocator != null) {
FindInLocator(predicate, results, currentLocator);
currentLocator = options == SearchMode.Local ? null : currentLocator.ParentLocator;
}
return new ReadOnlyLocator(results);
}
private void FindInLocator(Predicate<KeyValuePair<object, object>> predicate, Locator results, IReadableLocator currentLocator) {
foreach (KeyValuePair<object, object> kvp in currentLocator) {
if (!results.Contains(kvp.Key) && predicate(kvp)) {
results.Add(kvp.Key, kvp.Value);
}
}
}
這裡的 while 迴圈沿著父定位器鏈向上遞迴搜尋,完美詮釋了組合模式「統一處理單一與複合」的精髓。
ASP.NET 的 Control 類別正是組合模式的教科書級實現:
Control(Component)定義 Controls 集合屬性與 Add/Remove 方法Panel、GridView 等容器控制項(Composite)實作子控制項管理Button、Label 等葉子控制項(Leaf)繼承基類但不新增子項Render 方法自動遞迴呼叫子控制項的 Render這證明了組合模式在大型框架設計中的工業級價值。
ParentLocator)組合模式以「犧牲葉子節點的純粹性」換取「客戶端程式碼的極致簡潔」。它將「一對多」的容器關係轉化為「一對一」的統一介面,實現了客戶端與複雜物件結構的解耦。無論是透明式還是安全式,核心價值在於:讓遞迴成為實作細節,而非客戶端的負擔。在 .NET 生態中,從 Windows Forms 到 WPF、ASP.NET,組合模式無處不在,是每位架構師必須掌握的基石模式。