首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

.NET 設計模式(11):組合模式深度解析

     編輯:關於.NET

前言

在軟體開發中,樹狀結構無處不在——從檔案系統的目錄架構、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()。

.NET 框架中的經典實踐:Enterprise Library ObjectBuilder

在 Enterprise Library 2.0 的 ObjectBuilder 組件中,「定位器」採用了標準的組合模式:

  • Component:IReadableLocator 介面,定義查詢、遍歷、父節點導航等操作
  • Composite:Locator 類別,實作 IReadWriteLocator,管理子定位器鏈表
  • Leaf:ReadOnlyLocator,封裝內部定位器並屏蔽寫入操作

ObjectBuilder 定位器結構圖

關鍵程式碼片段展示了遞迴查找邏輯:


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 控制項體系的啟示

ASP.NET 的 Control 類別正是組合模式的教科書級實現:

  • Control(Component)定義 Controls 集合屬性與 Add/Remove 方法
  • Panel、GridView 等容器控制項(Composite)實作子控制項管理
  • Button、Label 等葉子控制項(Leaf)繼承基類但不新增子項
  • 頁面生命週期中,Render 方法自動遞迴呼叫子控制項的 Render

這證明了組合模式在大型框架設計中的工業級價值。

效能優化與實作要點

  1. 父節點反向追蹤:在子物件中保存父節點引用,便於向上導航(如 ObjectBuilder 的 ParentLocator)
  2. 快取技巧:若頻繁遍歷,可在複合節點快取扁平化的子節點列表
  3. 單一職責原則的妥協:透明式模式將管理方法放入 Component,技術上違反 SRP,但為了結構一致性屬必要代價
  4. 遞迴深度防護:極深樹結構需考慮堆疊溢位,可改用迭代器模式或顯式堆疊實現

總結

組合模式以「犧牲葉子節點的純粹性」換取「客戶端程式碼的極致簡潔」。它將「一對多」的容器關係轉化為「一對一」的統一介面,實現了客戶端與複雜物件結構的解耦。無論是透明式還是安全式,核心價值在於:讓遞迴成為實作細節,而非客戶端的負擔。在 .NET 生態中,從 Windows Forms 到 WPF、ASP.NET,組合模式無處不在,是每位架構師必須掌握的基石模式。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved