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

.NET 設計模式(10):裝飾模式——動態擴展物件功能的藝術

     編輯:關於.NET

前言:繼承的侷限與組合的自由

在物件導向設計中,我們常透過繼承來擴充功能。但繼承帶來的是靜態、編譯期決定的類別階層——當擴充功能呈排列組合爆炸式增長時,子類別數量將失控膨脹(經典的「子類別復子類別,子類別何其多」)。裝飾模式(Decorator Pattern)提供了一條优雅的出路:以物件組合取代類別繼承,在執行期動態為單一物件附加職責。

核心意圖與生活類比

GoF 定義:動態地給一個物件添加一些額外的職責。就增加功能來說,Decorator 模式相比生成子類別更為靈活。

生活中的經典類比是「畫框與畫作」:

  • 畫作本身(Component)可以獨立展示
  • 畫框(Decorator)包裹畫作,提供保護、裝飾功能
  • 玻璃(另一個 Decorator)可再包裹在畫框外
  • 最終懸掛在牆上的是「畫作+畫框+玻璃」的組合物件,但對牆壁而言,它仍是一個可掛載的「畫作」

畫框裝飾模式物件圖

從「日誌記錄」看演進歷程

初始設計:繼承爆炸的噩夢

假設我們有兩種基礎日誌實作:DatabaseLog、TextFileLog。需求新增「錯誤等級記錄」、「優先級記錄」兩個維度的擴充功能。若用繼承:


// 單一擴充功能:4 個子類別
DBErrorLog : DatabaseLog, IError
DBPriorityLog : DatabaseLog, IPriority
TFErrorLog : TextFileLog, IError
TFPriorityLog : TextFileLog, IPriority

// 組合擴充功能:再新增 4 個子類別
DBEPLog : DatabaseLog, IError, IPriority
TFEPLog : TextFileLog, IError, IPriority

繼承導致的類別爆炸

問題顯而易見:

  1. 違反單一職責原則:子類別同時承擔「日誌寫入」與「擴充功能」兩個變化原因
  2. 靜態綁定:客戶端無法在執行期動態選擇功能組合
  3. 類別數量呈指數級增長:M 種基礎實作 × N 種擴充功能的排列組合

裝飾模式重構:包裝而非繼承

關鍵洞見:將擴充功能封裝為獨立的「包裝器」類別,持有被裝飾物件的參考,並在轉呼叫前後插入額外邏輯。

步驟 1:定義抽象包裝基類


public abstract class LogWrapper : Log {
    protected Log _wrappedLog;
    public LogWrapper(Log log) { _wrappedLog = log; }
    public override void Write(string log) => _wrappedLog.Write(log); // 預設轉呼叫
}

步驟 2:具體裝飾類別實作單一擴充功能


public class LogErrorWrapper : LogWrapper {
    public LogErrorWrapper(Log log) : base(log) { }
    public override void Write(string log) {
        SetError();           // 前置:新增錯誤等級邏輯
        base.Write(log);      // 轉呼叫被裝飾物件
    }
    private void SetError() { /* 記錄錯誤等級實作 */ }
}

public class LogPriorityWrapper : LogWrapper {
    public LogPriorityWrapper(Log log) : base(log) { }
    public override void Write(string log) {
        SetPriority();        // 前置:新增優先級邏輯
        base.Write(log);
    }
    private void SetPriority() { /* 記錄優先級實作 */ }
}

裝飾模式類別結構圖

步驟 3:客戶端自由組合功能


public class Program {
    public static void Main() {
        Log log = new DatabaseLog();
        
        // 單一功能裝飾
        Log errorLog = new LogErrorWrapper(log);
        errorLog.Write("Message"); // 僅含錯誤等級
        
        // 多重裝飾:關鍵在於巢狀包裝!
        Log priorityLog = new LogPriorityWrapper(log);
        priorityLog.Write("Message"); // 僅含優先級
        
        // 組合功能:先裝飾錯誤等級,再裝飾優先級
        Log combined = new LogPriorityWrapper(new LogErrorWrapper(log));
        combined.Write("Message"); // 同時擁有錯誤等級與優先級
    }
}

精妙之處:LogPriorityWrapper 的建構子接受 Log 型別,而 LogErrorWrapper 繼承自 LogWrapper → Log,因此可作為參數傳入。這形成了「洋蔥狀」的包裝鏈,每層裝飾器只關注單一職責,執行順序由包裝順序決定。

.NET 框架中的經典應用

1. Stream 體系:教科書級裝飾模式

.NET 的 System.IO.Stream 及其衍生類別是裝飾模式的標準實現(省略了抽象裝飾角色):

  • Component:Stream(抽象基類)
  • ConcreteComponent:MemoryStream、FileStream、NetworkStream
  • ConcreteDecorator:BufferedStream(緩衝)、CryptoStream(加密)、GZipStream(壓縮)

Stream 裝飾模式結構


MemoryStream ms = new MemoryStream(data);
BufferedStream buffered = new BufferedStream(ms);        // 新增緩衝功能
CryptoStream encrypted = new CryptoStream(buffered, ...); // 再新增加密功能
// encrypted 同時擁有記憶體儲存、緩衝、加密三重能力

反編譯 BufferedStream 可見其核心結構:


public sealed class BufferedStream : Stream {
    private Stream _s;           // 被裝飾的 Stream
    private int _bufferSize;
    public BufferedStream(Stream stream) { _s = stream; }
    public override void Write(byte[] buffer, int offset, int count) {
        // 緩衝邏輯...
        _s.Write(buffer, offset, count); // 轉呼叫
    }
}

2. Enterprise Library DAAB:DbCommandWrapper

資料存取應用程式區塊(DAAB)中的 DbCommandWrapper 包裝 IDbCommand,統一提供參數處理、執行重試等横切關注點:

DbCommandWrapper 結構

裝飾模式的變體與靈活應用

GoF 指出裝飾模式可根據情境簡化:

  1. 無抽象 Component:若只有一個 ConcreteComponent,裝飾類可直接繼承它 無抽象 Component 變體
  2. 單一 ConcreteDecorator:可合併 Decorator 與 ConcreteDecorator 為單一類別 單一裝飾類別變體

優缺點權衡與適用場景

優點

  • 比繼承更靈活:執行期動態組合功能,支援任意排列組合
  • 避免子類別膨脹:M 個基礎類別 + N 個裝飾類別 = M×N 種組合可能
  • 符合開放封閉原則:新增功能只需新增裝飾類別,無需修改既有程式碼
  • 符合合成復用原則:優先使用物件組合而非繼承

缺點

  • 物件數量增加:每個裝飾層級都產生新物件,除錯時難以區分外觀相似的物件
  • 型別身份改變:裝飾後的物件不再是原始具體類別的實例(decorated is ConcreteComponent 為 false)
  • 過度設計風險:若擴充功能固定且極少,直接繼承更簡單

適用場景

  1. 需要動態、透明地為單一物件新增職責,且職責可動態撤銷
  2. 無法使用繼承(如類別為 sealed、或繼承會導致類別爆炸)
  3. 功能屬於「橫切關注點」(日誌、快取、驗證、加密、交易管理等)

總結

裝飾模式以「包裝」取代「繼承」,將「什麼時候擴充功能」的決定權從編譯期移交給執行期。它不是為了解決多重繼承問題,而是為了解決主體類別在多個方向上的擴充功能組合問題。在 .NET 生態中,從 Stream 到 ASP.NET Core Middleware 管線、Entity Framework 攔截器,裝飾模式的身影無處不在。掌握它,意味著你擁有了應對「功能組合爆炸」的利器。

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