裝飾者模式
動態地給物件新增職責,且不改變其結構。比繼承更靈活,是「包裝」的替代方案。
適用場景
- 需要在不建立大量子類別的情況下擴充功能
- 職責可動態新增/移除 (如視窗加捲軸、邊框、陰影)
- 基本功能的排列組合產生大量變體 (咖啡調味、串流處理管線)
- 避免「繼承爆炸」:
BaseWindow → ScrollableWindow → BorderedScrollableWindow → ...
UML 角色對應
| 角色 | C# 實作 | 說明 |
| Component (抽象元件) | 介面或抽象類別 | 定義核心合約 |
| ConcreteComponent (具體元件) | 實作介面的類別 | 被裝飾的核心物件 |
| Decorator (裝飾者基類) | 實作介面、持有 Component 參考 | 轉發呼叫、定義裝飾合約 |
| ConcreteDecorator (具體裝飾者) | 繼承 Decorator、新增行為 | 具體功能擴充 |
經典實作:咖啡飲料系統
1. 定義元件介面
public interface IBeverage
{
string Description { get; }
decimal Cost();
}
2. 具體元件:基礎飲料
public class Espresso : IBeverage
{
public string Description => "義式濃縮";
public decimal Cost() => 50m;
}
public class HouseBlend : IBeverage
{
public string Description => "招牌配方";
public decimal Cost() => 60m;
}
public class DarkRoast : IBeverage
{
public string Description => "深烘焙";
public decimal Cost() => 55m;
}
3. 裝飾者基類:持有被裝飾物件
public abstract class BeverageDecorator : IBeverage
{
protected readonly IBeverage _beverage;
protected BeverageDecorator(IBeverage beverage)
{
_beverage = beverage ?? throw new ArgumentNullException(nameof(beverage));
}
public virtual string Description => _beverage.Description;
public virtual decimal Cost() => _beverage.Cost();
}
4. 具體裝飾者:調味料
public class Mocha : BeverageDecorator
{
public Mocha(IBeverage beverage) : base(beverage) { }
public override string Description => $"{_beverage.Description}, 摩卡";
public override decimal Cost() => _beverage.Cost() + 10m;
}
public class Soy : BeverageDecorator
{
public Soy(IBeverage beverage) : base(beverage) { }
public override string Description => $"{_beverage.Description}, 豆漿";
public override decimal Cost() => _beverage.Cost() + 8m;
}
public class Whip : BeverageDecorator
{
public Whip(IBeverage beverage) : base(beverage) { }
public override string Description => $"{_beverage.Description}, 鮮奶油";
public override decimal Cost() => _beverage.Cost() + 5m;
}
5. 用戶端組合
public class CoffeeShop
{
public static void Main()
{
// 訂單 1:深烘焙 + 雙摩卡 + 鮮奶油
IBeverage drink1 = new DarkRoast();
drink1 = new Mocha(drink1);
drink1 = new Mocha(drink1);
drink1 = new Whip(drink1);
Console.WriteLine($"{drink1.Description} - ${drink1.Cost()}");
// 輸出:深烘焙, 摩卡, 摩卡, 鮮奶油 - $75
// 訂單 2:招牌配方 + 豆漿 + 摩卡 + 鮮奶油
IBeverage drink2 = new HouseBlend();
drink2 = new Soy(drink2);
drink2 = new Mocha(drink2);
drink2 = new Whip(drink2);
Console.WriteLine($"{drink2.Description} - ${drink2.Cost()}");
// 輸出:招牌配方, 豆漿, 摩卡, 鮮奶油 - $83
}
}
.NET Framework 內建裝飾者範例
| 基礎類別 | 裝飾者 | 新增功能 |
Stream | BufferedStream | 緩衝讀寫提升效能 |
Stream | CryptoStream | 加密/解密 |
Stream | GZipStream | 壓縮/解壓縮 |
Stream | StreamReader/Writer | 文字讀寫 |
TextReader | StringReader | 字串作為輸入來源 |
// 串流管線組合:檔案 → 緩衝 → GZip 壓縮 → 加密
using (FileStream fs = File.Create("data.enc"))
using (CryptoStream cs = new CryptoStream(fs, encryptor, CryptoStreamMode.Write))
using (GZipStream gz = new GZipStream(cs, CompressionMode.Compress))
using (StreamWriter sw = new StreamWriter(gz))
{
sw.WriteLine("敏感資料將被壓縮並加密儲存");
}
進階:透明裝飾者 vs 半透明裝飾者
| 類型 | 特徵 | 範例 |
| 透明 | 介面不變,用戶端不知情 | Stream 裝飾者、上述咖啡範例 |
| 半透明 | 新增額外介面/方法 | 視窗裝飾者新增 SetScrollBar() |
// 半透明裝飾者:新增特定功能介面
public interface IScrollable
{
void ScrollTo(int x, int y);
}
public class ScrollableWindow : WindowDecorator, IScrollable
{
public ScrollableWindow(IWindow window) : base(window) { }
public void ScrollTo(int x, int y) { /* 實作捲動 */ }
public override void Draw()
{
base.Draw();
DrawScrollBars();
}
}
裝飾者 vs 繼承 vs 策略模式
| 維度 | 繼承 | 裝飾者 | 策略模式 |
| 靈活度 | 編譯期固定 | 執行期動態組合 | 執行期可切換演算法 |
| 組合數 | 類別爆炸 | 線性包裝 | 策略類別獨立 |
| 狀態共享 | 子類別擁有自身狀態 | 每層可有獨立狀態 | 策略物件封裝狀態 |
| 型別身分 | 是基類型別 | 是基類型別 (透明) | 持有策略介面 |
最佳實務與注意事項
- 保持介面單一:裝飾者只擴充單一職責,避免巨大裝飾者
- 避免過度包裝:超過 3-4 層難以偵錯,考慮重構
- 實作
IDisposable 傳遞:若被裝飾物件可釋放,裝飾者應轉發 Dispose()
- 序列化考量:深層包裝物件序列化複雜,需自訂
- 不要破壞 LSP:裝飾者必須完全符合元件介面契約
本章小結
- 裝飾者模式以「包裝」取代「繼承」,實現動態功能組合
- 核心結構:
Component 介面 → ConcreteComponent → Decorator (持有 Component) → ConcreteDecorator
- .NET
Stream 體系是教科書級裝飾者應用
- 適用於功能疊加、管線處理、UI 構件增強等場景
- 現代 C# 也可用
Func<T, T> 管線或 Middleware 模式實現類似效果