《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模式
- 裝飾者模式——動態地將責任附加到對象上,若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案
要點
- 繼承屬於擴展形式之一,但不見得是達到彈性設計的最佳方式
- 我們的設計中,應該允許行為可以被擴展,而無需修改現有的代碼
- 組合和委託可用於在運行時動態地加上新的行為
- 除了繼承,裝飾者模式也可以讓我們擴展行為
- 裝飾者模式意味着一羣裝飾者類,這些類用來包裝具體組件
- 裝飾者類反映出被裝飾的組件類型(事實上,他們具有相同的類型,都經過接口或繼承實現)
- 裝飾者可以在被裝飾者的行為前面與/或後面加上自己的行為,甚至被裝飾者的行為整個取代掉,而達到特定的目的
- 你可以用無數個裝飾者包裝一個組件
- 裝飾者一般對組建的客户是透明的,除非客户程序依賴於組件的具體類型
- 裝飾者會導致設計中出現許多小對象,如果過度使用,會讓程序變得很複雜