C# Design Patterns (3) - Decorator
Decorator Pattern (裝飾模式)
裝飾模式可「動態」地給一個對象添加一些額外的職責,提供有別于「繼承」的另一種選擇。就擴展功能而言,Decorator Pattern 透過 Aggregation (聚合) 的特殊應用,降低了類與類之間的耦合度,會比單獨使用「繼承」生成子類更為靈活。
一般用「繼承」來設計子類的做法,會讓程序變得較僵硬,其對象的行為,是在「編譯」時期就已經「靜態」決定的,而且所有的子類,都會繼承到相同的行為;然而,若用「裝飾模式」以及 UML 的 Aggregation 的設計,來擴展對象的行為,就能彈性地 (flexible) 將多個「裝飾者」混合著搭配使用,而且是在「執行」時期「動態」地進行擴展。
此外,若用一般「繼承」的做法,每當對象需要新行為時,必須修改既有的代碼、重新編譯;但若透過「裝飾模式」,則無須修改既有代碼。
The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
- Design Patterns: Elements of Reusable Object-Oriented Software
圖 1 此圖為 Decorator 模式的經典 Class Diagram

上方圖 1 的 Class Diagram,以及「Shell (殼)」示例中,ConcreteComponent 即為此一模式中的「被裝飾者」,而 Decorator 抽象類及其具體子類 ConcreteDecoratorA、ConcreteDecoratorB 即為「裝飾者」。此外,這個模式中最核心的就是 Decorator 這個抽象類,它用一個以父類 Component 聲明的變量 component,實現 UML 中的 Aggregation (聚合,有的博文也統稱為「組合」或「合成」),亦即為指向 Component 對象的指針,達到我們前述 - 「動態」進行擴展的目的。
至于其設計的原理,以下我引用博客園一位前輩 Justin 兩年前所寫博文的一段內容 [1],這段是我認為對 Decorator 模式極優的說明。讀者亦可搭配參考上方代碼里我添加的注釋,稍后我會再補充說明。
--------------------------------------------------------
Decorator 是裝飾者模式里非常特殊的一個類,它既繼承于 Component【IS A關系】,又維護一個指向 Component 實例的引用【HAS A關系】,換個角度來說,Decorator 跟 Component 之間,既有動態組合關系,又有靜態繼承關系,WHY? 這里為什么要這么來設計?上面我們說過,組合的好處是可以在運行時給對象增加職責,Decorator【HAS A】Component 的目的,是讓 ConcreteDecorator 可以在運行時動態地給 ConcreteComponent 增加職責,這一點相對來說還比較好理解;那么 Decorator 繼承于 Component 的目的是什么?在這里繼承的目的只有一個,那就是可以統一「裝飾者」和「被裝飾者」的接口,換個角度來說,不管是 ConcretComponent 還是 ConcreteDecorator,它們都是最頂層 Component 基類,用戶代碼可以把它們統一看作 Component 來處理,這樣帶來的更深一層好處就是,「裝飾者」對象,對「被裝飾者」對象的功能職責擴展,對用戶代碼來說是完全透明的,因為用戶代碼引用的都是 Component,所以就不會因為「被裝飾者」對象在被裝飾后,引用它的用戶代碼發生錯誤,實際上不會有任何影響,因為裝飾前后,用戶代碼引用的都是 Component 類型的對象,這真是太完美了!「裝飾模式」通過繼承,實現統一了「裝飾者」和「被裝飾者」的接口,通過組合獲得了在運行時動態擴展「被裝飾者」對象的能力。
我們再舉個生活中的例子,俗話說“人在衣著馬在鞍”,把這用「裝飾模式」的語境翻譯一下,“人通過漂亮的衣服裝飾后,男人變帥了,女人變漂亮了;”。對應上面的類圖,這里「人」對應于 ConcreteComponent,而「漂亮衣服」則對應于 ConcreteDecorator;換個角度來說,人和漂亮衣服組合在一起【HAS A】,有了帥哥或美女,但是他們還是人【IS A】,還要做人該做的事情,但是可能會對異性更有吸引力了(擴展功能)!
--------------------------------------------------------
上方 Justin 前輩,其文章的「煮咖啡」示例 [1],是引用自 O'Reilly 出版社的「Head First 設計模式」這本書的第三章 [10]。該文的煮咖啡類圖中,HouseBlend (家常咖啡)、DarkRoast (深度烘培咖啡)、Espresso (意大利特濃咖啡)、Decaf (無咖啡因咖啡),這四種咖啡 (飲料),即為「被裝飾者」,等同本帖上圖 1 中的 ConcreteComponent 類;該文類圖中的 CondimentDecorator 抽象類,等同本帖上圖 1 中最重要的 Decorator 抽象類,亦即「裝飾者」的抽象定義;該文類圖中的 Milk、Mocha、Soy、Whip 這四種調料 (調味品),即為具體的「裝飾者」,亦即在本帖一開始提到,這四種調料,可彈性地 (flexible) 混合著搭配使用,而且是在「執行」時期「動態」地進行擴展,亦即動態地裝飾 HouseBlend、DarkRoas、Espresso、Decaf 這四種咖啡。
接下來,我們用另一個簡單的例子來實現 Decorator 模式,并改用 ASP.NET 網頁程序來實現。類圖及代碼如下方圖 2 所示,執行結果如下圖 3 所示。
此為一個西餐牛排館的計費程序,這間牛排館有兩種主菜 - 牛排和豬排,此為「被裝飾者」;有四種副菜 - 面條、生菜沙拉、飲料 (熱飲或冷飲)、甜點,此為「裝飾者」。客人點餐時,可點一種主菜,副菜可點一份、可點多份,也可不點 (有彈性地將多個「裝飾者」混合著搭配);每樣主菜和副菜都有各自的價格,全部相加后,即為一或多位客人的用餐費用。而且我們希望,將來這個計費程序寫好后,未來即使要修改價格,或添加新的主菜和副菜時,都不用再修改既有的程序。
圖 2 示例 02_Steak.aspx.cs 的 Class Diagram

圖 3 示例 02_Steak.aspx.cs 的執行結果。擴展功能時,主菜和副菜可任意混合搭配使用
原理與本帖之前提過的相同,將來店里若要推出 羊排、魚排、…等新的主菜 (被裝飾者) 時,只要增加一個類,去繼承 Meal 這個抽象的基類即可;若要推出新的副菜 (裝飾者) 也一樣,只要增加一個類,去繼承 CondimentDecorator 抽象類即可。我們透過這個重要的 CondimentDecorator 抽象類,實現 UML 的 Aggregation (聚合) 觀念,以「裝飾模式」取代傳統「繼承」的做法,也同時降低了「主菜」、「副菜」這兩種類之間的耦合度。
這個 CondimentDecorator 抽象類的作用,還可將未來所有種類的副菜,一些「共同的」行為或職責寫在里面。而且,它還可用來區分哪些類是主菜,哪些類是副菜,因為主菜、副菜都是繼承自 Meal 這個最頂層的基類,若沒有這個 CondimentDecorator 抽象類,將來 Class Diagram 變得很復雜時,會不易分辨某個類是主菜或副菜。
此外,也因為主菜 (被裝飾者)、副菜 (裝飾者) 都是繼承自 Meal 這個最頂層的基類,所以客戶端程序 (Page_Load) 在引用他們的對象時,把 new 出來的主菜實例、new 出來的副菜實例,再拋入其他副菜實例的構造函數中,也會相容、不會發生錯誤,在相互引用和「運行」時期的「動態」擴展上,變得很有彈性。
最后補充一點,Decorator 模式中,裝飾者類對象,在組合時的「順序」亦很重要。本帖圖 1 中,ConcreteDecoratorA、ConcreteDecoratorB、ConcreteDecoratorC、... 等具體裝飾者,在動態添加和組合時,在某些實際應用中必須依照一定的順序,若順序相反可能會導致程序執行結果不合理,但本帖所提供的幾個例子中并不需要考慮到這點。
--------------------------------------------------------
Decorator Pattern 適用的情景:
- 你擁有一個已存在的組件類,卻無法繼承它 (subclassing)。
- 能夠動態地為對象添加職責 (添加狀態和行為)。
- 改變類中的成員和行為,但不影響其他對象。
- 希望能便于職責的撤消。
- 不想用「繼承」來擴展行為。其中一種原因是避免當一些功能要交叉搭配引用時,單獨用「繼承」來設計會產生太多的子類、太復雜的類圖結構 [1], [10];另一種考量可能是因為類的定義被隱藏,或類的定義不能用于生成子類。
Decorator Pattern 的優點:
- 可避免單獨使用「繼承」時,在擴展時不夠彈性,且可能衍生過多的子類。
- 擴展時不需要修改既有的代碼。
- 可在執行時期,動態地添加新行為 (職責)。
Decorator Pattern 的缺點:
- 可能會在程序中出現許多的小型類,亦即需要編寫很多 ConcreteDecorator 類 (具體裝飾者)。
- 若過度使用 Decorator 模式,會讓程序邏輯變得很復雜。
- 別人較不易理解設計方式及代碼,排查故障、追蹤和調試也比較困難。
Decorator Pattern 的其他特性:
- 每個要裝飾的功能,都放在單獨的類中。
- 我們可以用無數個裝飾者,去包裝一個組件。
- 「裝飾者」可以在「被裝飾者」前面或后面,添加自己的行為,甚至將「被裝飾者」的行為整個取代掉,以達到特定的目的。
- 「被裝飾者」并不需要知道它已經被「裝飾」過了,亦即 Component 類 (對象) 并不需要知道 Decorator 類 (對象) 的存在,且 Decorator 也僅僅認識 Component。
--------------------------------------------------------
最后再補充,在 Java I/O、.NET I/O、.NET System.Windows.Controls 命名空間中,很多地方都有用到 Decorator 模式來設計 [2], [5], [12]。例如我們查詢 MSDN Library,會發現 .NET 3.x 版本的 Windows Forms 其 API 里,甚至還有增加一個新的 Decorator 類,如下:
System.Object
System.Windows.Threading.DispatcherObject
System.Windows.DependencyObject
System.Windows.Media.Visual
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.Decorator
System.Windows.Controls.Viewbox
System.Windows.Controls.Decorator 類:
提供在單個子元素(如 Border 或 Viewbox)上或周圍應用效果的元素的基類。
http://msdn.microsoft.com/zh-cn/library/system.windows.controls.decorator.aspx
http://msdn.microsoft.com/zh-cn/library/system.windows.controls.decorator(VS.85).aspx
以下列出 Decorator Pattern,用于實際程序的四種方式 [12]:
- 裝飾模式非常適合于「圖形程序」,亦適合于「視頻和聲音」,比如:視頻流可用不同的比率進行壓縮,而聲音可以輸入給同聲傳譯服務。
- 裝飾模式如此有用,以致于 .NET 3.0 中,已經有實際的 Decorator 類了。如剛才提到的,在 System.Windows.Controls 命名空間中,提供了一些基類,可用于給其他的圖形元素添加特殊效果,如:Border 類和 Viewbox 類。
-
在如今的移動設備上,Web 瀏覽器及其他應用中,也大量運用 Decorator Pattern。比如這些設備上可以創建適合小屏幕顯示的對象,跟標準桌面機上的瀏覽器相比,這些屏幕可能也含有滾動條,但不含有標題欄 [6]。
- 從更一般的層次上,.NET 的 I/O API 中,到處都是裝飾模式 [6],如下方的體系:
System.Object
System.MarshalByRefObject
System.IO.Stream
System.IO.BufferedStream
System.IO.FileStream
System.IO.MemoryStream
System.Net.Sockets.NetworkStream
System.Security.Cryptography.CryptoStream
上方體系的最后五個子類,有的就裝飾了 Steram 這個父類 (如:BufferedStream 類),因為它們從它繼承,同時又含有 Stream 類的實例,而且這個實例是子類被構造時傳遞進去的 (如同本帖第二個西餐店的示例中,副菜的構造函數的寫法)。如下方 MSDN Library 代碼的 BufferedStream 類,其構造函數的第一個參數,即為其父類 System.IO.Stream 的變量 stream。
BufferedStream 構造函數:
public BufferedStream(
Stream stream
)
http://msdn.microsoft.com/zh-cn/library/system.io.bufferedstream.bufferedstream(VS.80).aspx
--------------------------------------------------------
本帖的最后,提供一位 Java 大師 - 結城浩,所繪制的 Decorator Pattern 趣味四格漫畫 [9],原地址如下:
Giko 貓談 DP 四格漫畫:
http://www.hyuki.com/dp/cat_Decorator.html
http://www.javaworld.com.tw/jute/post/view?bid=44&id=40923&sty=3
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
∧_∧ 敲敲敲 ╱
( ) ∧ ∧ < System.IO.Stream 是個典型的例子..恩....。
( ) (,,゚Д゚) ╲____________
______ (つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
____________
╱
| 喔~、是 Decorator Pattern 嗎?
╲ __ __________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 你你是誰? ...有..有什么事嬤你?・・・
( ⊃ ) (゚Д゚;) ╲____________
________(つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________
╱
| 將 Concrete Decorator 重迭起來達到想要的目的..恩
╲ __ ________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 恩...恩恩..沒錯....
( ) (;゚Д゚) ╲____________
_____ (つ_つ____
| 日∇╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
__________
╱
| 亂做一堆細微的類的人就是你.恩.
╲ __ ________
|╱
∧_∧ ╱
( ・∀・) ∧ ∧ < 不..不是我..!!
( ⊃ ) (゚Д゚;) ╲____________
_____(つ_つ____
| 日∇ ╲|ThinkPad|╲
| ========= ╲
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -