意圖(Intent)#

動態地為物件附加額外的職責。Decorator 提供了比子類別化更彈性的功能擴展方式。

別名(Also Known As)#

Wrapper

動機(Motivation)#

有時我們想為個別物件(而非整個類別)增加職責。例如,GUI 工具組應該能讓任何 UI 元件加上邊框或捲動功能。

用繼承來加上邊框,會讓每個子類別實例都帶有邊框——選擇是靜態的,客戶端無法控制何時裝飾。更彈性的做法是用另一個物件來「包裹」原始元件,這個包裹物件就是 decorator

Decorator 的關鍵特性:

  • 符合被裝飾元件的介面,對客戶端透明
  • 將請求轉發給內部元件,可在轉發前後執行額外操作
  • 可以遞迴巢套,堆疊無限數量的職責

例如:一個 TextView 先被 ScrollDecorator 包裹加上捲動功能,再被 BorderDecorator 包裹加上邊框,就得到一個有邊框的可捲動文字視圖。

Decorator 的核心優勢在於組合優於繼承:透過物件組合而非類別繼承來擴展功能,職責可以在執行期動態添加和移除,且不同裝飾器可以自由混搭組合。

適用場景(Applicability)#

  • 需要動態、透明地為個別物件加上職責,而不影響其他物件
  • 需要可撤回的職責擴展
  • 用子類別化來擴展不切實際——可能的獨立擴展太多,會導致子類別數量爆炸;或者類別定義被隱藏、無法繼承

結構(Structure)#

Decorator 持有一個 Component 的引用,其介面與 Component 一致。ConcreteDecorator 繼承 Decorator 並加上額外的職責。客戶端透過 Component 介面操作,不需知道裝飾器的存在。

classDiagram
    class Component {
        <<interface>>
        +Operation()
    }
    class ConcreteComponent {
        +Operation()
    }
    class Decorator {
        -component
        +Operation()
    }
    class ConcreteDecoratorA {
        -addedState
        +Operation()
    }
    class ConcreteDecoratorB {
        +Operation()
        +AddedBehavior()
    }
    Component <|.. ConcreteComponent
    Component <|.. Decorator
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB
    Decorator o--> Component

參與者(Participants)#

參與者範例職責
ComponentVisualComponent定義可被動態加上職責的物件介面
ConcreteComponentTextView被附加額外職責的具體物件
Decorator-持有 Component 引用,定義符合 Component 的介面
ConcreteDecoratorBorderDecorator、ScrollDecorator為元件增加具體的職責

協作方式(Collaborations)#

  • Decorator 將請求轉發給其 Component 物件,可在轉發前後執行額外操作

優缺點(Consequences)#

  • 比靜態繼承更彈性:職責可在執行期動態添加和移除,不需為每種組合建立新類別。同一種裝飾可以套用兩次(如雙重邊框)
  • 避免功能臃腫的高層類別:採用按需付費(pay-as-you-go)的方式,從簡單類別開始,用 Decorator 逐步增加功能
  • 物件身分問題:被裝飾的物件與原始物件不是同一個物件,不應依賴物件身分(object identity)
  • 大量小物件:使用 Decorator 的設計會產生許多外觀相似的小物件,差異僅在於連接方式,可能增加學習和除錯難度

實作要點(Implementation)#

  • 介面一致性:Decorator 必須符合 Component 的介面,所有 ConcreteDecorator 需繼承共同的類別
  • 省略抽象 Decorator 類別:只需要加一種職責時,可以直接將轉發邏輯合併到 ConcreteDecorator 中
  • 保持 Component 類別輕量:Component 應專注於定義介面,不應儲存資料。若 Component 過於龐大,Decorator 也會變得笨重
  • 改變「外皮」vs. 改變「內臟」
    • Decorator 從外部改變物件行為(改變外皮)
    • Strategy 從內部改變物件行為(改變內臟)
    • 當 Component 本身就很重量級時,Strategy 可能是更好的選擇

當 Component 類別已經很龐大時,考慮使用 Strategy 取代 Decorator。Strategy 讓元件將行為委派給獨立的策略物件,策略物件可以有自己專用的介面,即使 Component 很重也能保持策略的輕量。MacApp 和 Bedrock 的「adorner」和「behavior」物件就是這種做法。

已知應用(Known Uses)#

  • InterViews、ET++、ObjectWorks\Smalltalk:廣泛使用 Decorator 為 widget 加上圖形裝飾
  • InterViews DebuggingGlyph:在轉發 layout 請求前後輸出除錯資訊
  • ParcPlace Smalltalk PassivityWrapper:啟用或停用使用者互動
  • ET++ Stream 類別:Stream 是基礎抽象,StreamDecorator 子類別如 CompressingStream(壓縮)、ASCII7Stream(7-bit ASCII 轉換)可以自由組合。例如:FileStream 先包一層 CompressingStream 再包一層 ASCII7Stream

相關模式(Related Patterns)#

  • Adapter:Adapter 改變物件的介面;Decorator 只改變職責,不改變介面
  • Composite:Decorator 可視為只有一個元件的退化 Composite,但目的不同——Decorator 是加上職責,不是用來聚合物件
  • Strategy:Decorator 改變外皮,Strategy 改變內臟——兩種改變物件的替代方式