动态

详情 返回 返回

《Head First設計模式》讀書筆記 —— 裝飾者模式 - 动态 详情

《Head First設計模式》讀書筆記
相關代碼:Vks-Feng/HeadFirstDesignPatternNotes: Head First設計模式讀書筆記及相關代碼

給愛用繼承的人一個全新的設計眼界

本節用例

Starbuzz咖啡因迅速擴展,準備更新訂單系統,以合乎其飲料供應需求

原有類設計如下:
原有的類設計.png

在購買咖啡時,可以在其中加入各種調料,系統需要考慮調料部分計算費用

第一版嘗試——枚舉所有情況

第一版嘗試(枚舉所有情況):簡直是“類爆炸”
第一個嘗試.png

這是一個“維護惡夢”

  • 當有飲料或者調料價格變動,或有新調料出現……

第二版嘗試——實例變量&繼承

從基類Beverage下手,添加實例變量代表是否加上調料
帶有實例變量的基類.png

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

哪些需求或因素改變時會影響這個設計?

  • 調料價錢的改變會使我們更改現有代碼
  • 一旦出現新的調料,我們就需要加上新的方法,並改變超類中的cost()方法
  • 當出現新飲料時,明顯不相配的調料也會被繼承
  • 當顧客想要雙倍摩卡咖啡時,如何處理……

回顧:組合和委託

儘管繼承威力強大,但是它並不總是能實現最有彈性和最好維護的設計。而通過利用組合(composition)和委託(delegation)可以在運行時具有繼承行為的效果

  • 利用繼承設計子類的行為:
    • 在編譯時靜態決定
    • 所有子類都會繼承到相同的行為
  • 利用組合擴展對象的行為:
    • 動態地進行擴展
    • 可將在設計超類時還沒有想到的職責加到對象上,且不用修改原有代碼

利用組合維護代碼:通過動態地組合對象,可以寫新的代碼添加新功能,而無需修改現有代碼,引進bug或者產生意外副作用的機會將大幅度減少

開放-關閉原則

代碼應該如同晚霞中的蓮花一樣地關閉(免於改變),如同晨曦中的蓮花一樣地開放(能夠擴展)

HeadFirst設計原則4 :類應該對擴展開放,對修改關閉

開放:通過用任何想要的行為擴展類,應對需求的改變
關閉:已經花了很多時間確保代碼的正確,修改現有代碼可能會導致許多問題

目標:允許類容易擴展,在不修改現有代碼的情況下,就可搭配新的行為。這樣的設計具有彈性,可以應對改變,接受新的功能來應對改變的需求

Q:“對擴展開放,對修改關閉”聽上去很矛盾,設計如何兼顧兩者?
A:有一些聰明的OO技巧,允許系統在不修改代碼的情況下,進行功能擴展。例如觀察者模式中,通過加入新的觀察者,我們可以在任何時候擴展主題,且不需要向主題中添加代碼。

Q:如何將某件東西設計成可以擴展,又禁止修改?
A:學習裝飾者模式

Q:如何讓設計的每個部分都遵循“開放-關閉”原則
A:通常很難辦到,這需要花費很多時間和努力。遵循開放-關閉原則,通常會引入新的抽象層次,增加代碼的複雜度。我們需要把注意力集中在設計中最有可能改變的地方,然後在那裏應用開放-關閉原則

認識裝飾者模式

為了解決Starbuzz的問題,我們採用與上述不一樣的做法:以飲料為主體,然後在運行時以調料來“裝飾”(decorate)飲料

例如:顧客想要摩卡和奶泡深焙咖啡

  1. 拿一個深焙咖啡(DarkRoast)對象
  2. 以摩卡(Mocha)對象裝飾它
  3. 以奶泡(Whip)對象裝飾它
  4. 調用cost()方法,並依賴委託(delegate)將調料的價錢加上去

以裝飾者構造飲料訂單

  1. 以DarkRoast對象開始
    order1.png

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

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

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

小結

  • 裝飾者和被裝飾對象有相同的超類型
  • 你可以用一個或多個裝飾者包裝一個對象
  • 既然裝飾者和被裝飾對象有相同的超類型,所以在任何需要原始對象(被包裝的)的場合,可以用裝飾過的對象代替它
  • ==裝飾者可以在所委託被裝飾者的行為之前與/或之後,加上自己的行為,以達到特定的目的
  • 對象可以在任何時候被裝飾,所以可以在運行時動態地、不限量地使用你喜歡的裝飾者來裝飾對象

定義裝飾者模式

HeadFirst設計模式3-裝飾者模式

裝飾者模式動態地將責任附加到對象上,若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案

裝飾者模式.png

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

注意:

  • “繼承的目的”:此處CondimentDecorator擴展自Beverage類時用到了繼承,但是這裏“繼承的重點”是達到“類型匹配”的目的(因為裝飾者和被裝飾者必須是一樣的類型),而非利用繼承獲得“行為”
  • 新行為的來源:將裝飾者與組件組合時,就是在加入新的行為。即行為通過組合對象得來

一言以蔽之:繼承超類是為了有正確的類型,而不是繼承他的行為。行為來自裝飾者和基礎組件,或與其他裝飾者之間的組合關係。

好處:

  • 使用對象組合,可以把飲料和調料更有彈性地加以混合與匹配,十分方便
  • 組合而非繼承,實現“運行時”而非“編譯時”
  • 無需修改現有代碼

系統實現

代碼見開篇處倉庫地址

Q:如果針對特定種類的具體組件,做特殊的時,這樣的設計是否恰當。(例如,針對HouseBlend打折)
A:如果代碼寫成針對具體的組件類型,那麼裝飾者就會導致程序出問題,只有在針對抽象組件類型編程時,才不會因為裝飾者而受到影響。如果的確需要針對特定的具體組件編程,就應該重新思考應用架構,以及裝飾者模式是否合適。

Q:對於使用到飲料的某些客户來説,會不會容易不使用到最外面的裝飾着呢?(即層層包裝時產生了很多對象,有可能最終用錯了,用的不是最外圈的)
A:使用裝飾者模式的確必須管理更多對象,所以犯下這種編碼錯誤的機會會增加。但是裝飾者通常是用其他類似於工廠或生成器這樣的模式創建的,它們會“封裝的很好”,所以不會有這種問題。

Q:裝飾者知道這一連串裝飾鏈條中其他裝飾者的存在嗎?
A:裝飾者該做的事就是增加行為到被包裝對象上,當需要窺視裝飾者鏈中的每一個裝飾者事,這就超出他們的天賦了。但是可以通過其他方式實現需要藉此完成的功能。

Java I/O中的裝飾者

JavaIO.png
JavaIO1.png

Java I/O引出了裝飾者模式的一個“缺點”:利用裝飾者模式,常造成設計中有大量的小類,數量眾多,可能會造成使用此API程序員的困擾

裝飾者模式優缺點

優點 缺點 解決
具有為設計注入彈性的能力 有時會在設計中加入大量的小類,會導致別人不容易瞭解其設計方式 花點功夫對設計進行學習
可以透明地插入裝飾者,客户程序甚至不知道它是在和裝飾者打交道 人們在客户代碼中依賴某種特殊類型,然後忽然導入到裝飾者,卻沒有周詳地考慮一切,就會出現問題 在插入裝飾者是,必須要小心謹慎
/ 採用裝飾者在實例化組件時,將增加代碼地複雜度。一旦使用裝飾者模式,不只需要實例化組件,還要把此組件包裝進裝飾者中 採用工廠(Factory)模式和生成器(Builder)模式來解決此問題

總結

OO基礎

  • 抽象
  • 封裝
  • 多態
  • 繼承

OO原則

  • 封裝變化
  • 多用組合,少用繼承
  • 針對接口編程,不針對實現編程
  • 為交互對象之間的鬆耦合設計而努力
  • 對擴展開放,對修改關閉

OO模式

  • 裝飾者模式——動態地將責任附加到對象上,若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案

要點

  • 繼承屬於擴展形式之一,但不見得是達到彈性設計的最佳方式
  • 我們的設計中,應該允許行為可以被擴展,而無需修改現有的代碼
  • 組合和委託可用於在運行時動態地加上新的行為
  • 除了繼承,裝飾者模式也可以讓我們擴展行為
  • 裝飾者模式意味着一羣裝飾者類,這些類用來包裝具體組件
  • 裝飾者類反映出被裝飾的組件類型(事實上,他們具有相同的類型,都經過接口或繼承實現)
  • 裝飾者可以在被裝飾者的行為前面與/或後面加上自己的行為,甚至被裝飾者的行為整個取代掉,而達到特定的目的
  • 你可以用無數個裝飾者包裝一個組件
  • 裝飾者一般對組建的客户是透明的,除非客户程序依賴於組件的具體類型
  • 裝飾者會導致設計中出現許多小對象,如果過度使用,會讓程序變得很複雜
user avatar hedzr 头像 shimiandehoutao 头像 XY-Heruo 头像 dennyLee2025 头像
点赞 4 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.