《Head First設計模式》讀書筆記 —— 裝飾者模式
《Head First設計模式》讀書筆記
相關代碼:Vks-Feng/HeadFirstDesignPatternNotes: Head First設計模式讀書筆記及相關代碼
給愛用繼承的人一個全新的設計眼界
本節用例
Starbuzz咖啡因迅速擴展,準備更新訂單系統,以合乎其飲料供應需求
原有類設計如下:

在購買咖啡時,可以在其中加入各種調料,系統需要考慮調料部分計算費用
第一版嘗試——枚舉所有情況
第一版嘗試(枚舉所有情況):簡直是“類爆炸”

這是一個“維護惡夢”
- 當有飲料或者調料價格變動,或有新調料出現……
第二版嘗試——實例變量&繼承
從基類Beverage下手,添加實例變量代表是否加上調料

再加入子類,每個類表示菜單的一種飲料

哪些需求或因素改變時會影響這個設計?
- 調料價錢的改變會使我們更改現有代碼
- 一旦出現新的調料,我們就需要加上新的方法,并改變超類中的cost()方法
- 當出現新飲料時,明顯不相配的調料也會被繼承
- 當顧客想要雙倍摩卡咖啡時,如何處理……
回顧:組合和委托
盡管繼承威力強大,但是它并不總是能實現最有彈性和最好維護的設計。而通過利用組合(composition)和委托(delegation)可以在運行時具有繼承行為的效果
- 利用繼承設計子類的行為:
- 在編譯時靜態決定
- 所有子類都會繼承到相同的行為
- 利用組合擴展對象的行為:
- 動態地進行擴展
- 可將在設計超類時還沒有想到的職責加到對象上,且不用修改原有代碼
利用組合維護代碼:通過動態地組合對象,可以寫新的代碼添加新功能,而無需修改現有代碼,引進bug或者產生意外副作用的機會將大幅度減少
開放-關閉原則
代碼應該如同晚霞中的蓮花一樣地關閉(免于改變),如同晨曦中的蓮花一樣地開放(能夠擴展)
HeadFirst設計原則4 :類應該對擴展開放,對修改關閉
開放:通過用任何想要的行為擴展類,應對需求的改變
關閉:已經花了很多時間確保代碼的正確,修改現有代碼可能會導致許多問題
目標:允許類容易擴展,在不修改現有代碼的情況下,就可搭配新的行為。這樣的設計具有彈性,可以應對改變,接受新的功能來應對改變的需求
Q:“對擴展開放,對修改關閉”聽上去很矛盾,設計如何兼顧兩者?
A:有一些聰明的OO技巧,允許系統在不修改代碼的情況下,進行功能擴展。例如觀察者模式中,通過加入新的觀察者,我們可以在任何時候擴展主題,且不需要向主題中添加代碼。
Q:如何將某件東西設計成可以擴展,又禁止修改?
A:學習裝飾者模式
Q:如何讓設計的每個部分都遵循“開放-關閉”原則
A:通常很難辦到,這需要花費很多時間和努力。遵循開放-關閉原則,通常會引入新的抽象層次,增加代碼的復雜度。我們需要把注意力集中在設計中最有可能改變的地方,然后在那里應用開放-關閉原則
認識裝飾者模式
為了解決Starbuzz的問題,我們采用與上述不一樣的做法:以飲料為主體,然后在運行時以調料來“裝飾”(decorate)飲料
例如:顧客想要摩卡和奶泡深焙咖啡
- 拿一個深焙咖啡(DarkRoast)對象
- 以摩卡(Mocha)對象裝飾它
- 以奶泡(Whip)對象裝飾它
- 調用cost()方法,并依賴委托(delegate)將調料的價錢加上去
以裝飾者構造飲料訂單
-
以DarkRoast對象開始

- DarkRoast繼承自Beverage,且有一個用來計算飲料價錢的
cost()方法
- DarkRoast繼承自Beverage,且有一個用來計算飲料價錢的
-
顧客想要摩卡(Mocha),所以建立一個Mocha對象,并用它將DarkRoast對象包(wrap)起來

- Mocha對象是一個裝飾者,它的類型“反映”了它所裝飾的對象(本例中就是Beverage)所謂“反映”指兩者類型一致
- 所以Mocha也有一個
cost()方法。通過多態,也可以將Mocha所包裹的任何Beverage當成是Beverage(因為Mocha是Beverage的子類型)
-
顧客想要奶泡(Whip),所以需要建立一個Whip裝飾者,并用它將Mocha對象包起來。

- Whip是一個裝飾者,所以它也反映了DarkRoast類型,并包括一個
cost方法 - 所以被Mocha和Whip包起來的DarkRoast對象仍然是一個Beverage,任然可以具有DarkRoast的一切行為,包括調用它的
cost()方法
- Whip是一個裝飾者,所以它也反映了DarkRoast類型,并包括一個
-
算錢:通過調用最外圈裝飾者(Whip)的
cost()就可以辦得到。Whip的cost()會先委托它裝飾的對象(也就是Mocha)計算出價錢,然后再加上奶泡的價錢

小結
- 裝飾者和被裝飾對象有相同的超類型
- 你可以用一個或多個裝飾者包裝一個對象
- 既然裝飾者和被裝飾對象有相同的超類型,所以在任何需要原始對象(被包裝的)的場合,可以用裝飾過的對象代替它
- ==裝飾者可以在所委托被裝飾者的行為之前與/或之后,加上自己的行為,以達到特定的目的
- 對象可以在任何時候被裝飾,所以可以在運行時動態地、不限量地使用你喜歡的裝飾者來裝飾對象
定義裝飾者模式
HeadFirst設計模式3-裝飾者模式
裝飾者模式動態地將責任附加到對象上,若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案

把裝飾者模式用于Starbuzz系統,得到類圖如下:

注意:
- “繼承的目的”:此處CondimentDecorator擴展自Beverage類時用到了繼承,但是這里“繼承的重點”是達到“類型匹配”的目的(因為裝飾者和被裝飾者必須是一樣的類型),而非利用繼承獲得“行為”
- 新行為的來源:將裝飾者與組件組合時,就是在加入新的行為。即行為通過組合對象得來
一言以蔽之:繼承超類是為了有正確的類型,而不是繼承他的行為。行為來自裝飾者和基礎組件,或與其他裝飾者之間的組合關系。
好處:
- 使用對象組合,可以把飲料和調料更有彈性地加以混合與匹配,十分方便
- 組合而非繼承,實現“運行時”而非“編譯時”
- 無需修改現有代碼
系統實現
代碼見開篇處倉庫地址
Q:如果針對特定種類的具體組件,做特殊的時,這樣的設計是否恰當。(例如,針對HouseBlend打折)
A:如果代碼寫成針對具體的組件類型,那么裝飾者就會導致程序出問題,只有在針對抽象組件類型編程時,才不會因為裝飾者而受到影響。如果的確需要針對特定的具體組件編程,就應該重新思考應用架構,以及裝飾者模式是否合適。
Q:對于使用到飲料的某些客戶來說,會不會容易不使用到最外面的裝飾著呢?(即層層包裝時產生了很多對象,有可能最終用錯了,用的不是最外圈的)
A:使用裝飾者模式的確必須管理更多對象,所以犯下這種編碼錯誤的機會會增加。但是裝飾者通常是用其他類似于工廠或生成器這樣的模式創建的,它們會“封裝的很好”,所以不會有這種問題。
Q:裝飾者知道這一連串裝飾鏈條中其他裝飾者的存在嗎?
A:裝飾者該做的事就是增加行為到被包裝對象上,當需要窺視裝飾者鏈中的每一個裝飾者事,這就超出他們的天賦了。但是可以通過其他方式實現需要借此完成的功能。
Java I/O中的裝飾者


Java I/O引出了裝飾者模式的一個“缺點”:利用裝飾者模式,常造成設計中有大量的小類,數量眾多,可能會造成使用此API程序員的困擾
裝飾者模式優缺點
| 優點 | 缺點 | 解決 |
|---|---|---|
| 具有為設計注入彈性的能力 | 有時會在設計中加入大量的小類,會導致別人不容易了解其設計方式 | 花點功夫對設計進行學習 |
| 可以透明地插入裝飾者,客戶程序甚至不知道它是在和裝飾者打交道 | 人們在客戶代碼中依賴某種特殊類型,然后忽然導入到裝飾者,卻沒有周詳地考慮一切,就會出現問題 | 在插入裝飾者是,必須要小心謹慎 |
| / | 采用裝飾者在實例化組件時,將增加代碼地復雜度。一旦使用裝飾者模式,不只需要實例化組件,還要把此組件包裝進裝飾者中 | 采用工廠(Factory)模式和生成器(Builder)模式來解決此問題 |
總結
OO基礎
- 抽象
- 封裝
- 多態
- 繼承
OO原則
- 封裝變化
- 多用組合,少用繼承
- 針對接口編程,不針對實現編程
- 為交互對象之間的松耦合設計而努力
- 對擴展開放,對修改關閉
OO模式
- 裝飾者模式——動態地將責任附加到對象上,若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案
要點
- 繼承屬于擴展形式之一,但不見得是達到彈性設計的最佳方式
- 我們的設計中,應該允許行為可以被擴展,而無需修改現有的代碼
- 組合和委托可用于在運行時動態地加上新的行為
- 除了繼承,裝飾者模式也可以讓我們擴展行為
- 裝飾者模式意味著一群裝飾者類,這些類用來包裝具體組件
- 裝飾者類反映出被裝飾的組件類型(事實上,他們具有相同的類型,都經過接口或繼承實現)
- 裝飾者可以在被裝飾者的行為前面與/或后面加上自己的行為,甚至被裝飾者的行為整個取代掉,而達到特定的目的
- 你可以用無數個裝飾者包裝一個組件
- 裝飾者一般對組建的客戶是透明的,除非客戶程序依賴于組件的具體類型
- 裝飾者會導致設計中出現許多小對象,如果過度使用,會讓程序變得很復雜

浙公網安備 33010602011771號