運行時擴展,比編譯時繼承威力更大
裝飾對象,給愛用繼承的人一個全新的設計眼界
星巴茲(Starbuzz)咖啡訂單系統1
(實錘了 是我買不起的樣子)
星巴茲咖啡的擴張速度太快了,他們準備更新訂單系統
他們之前設計的類是這樣的
// 飲料類,店內所有飲料繼承此類
abstract class Beverage{
// 咖啡店的宣傳標語
String description;
public void getDesString() {
System.out.println(this.description);
}
// 咖啡的花銷,子類自己實現
public abstract void cost();
}
由於購買咖啡的時候會加入很多調料,比如牛奶、椰果等等,這樣就組成了很多不同的咖啡種類,價格也就不一樣
一旦調料多起來,類設計就會變成這樣:
類爆炸!這簡直是維護噩夢,如果牛奶價格上漲,所有加了牛奶的咖啡種類的cost()
都要重寫,這可太刺激了
利用實例變量和繼承
爲了不出現類爆炸的情況,我們使用實例變量和繼承,在基類Beverage
種添加代表每種調料的boolean
類型實例變量
子類的cost()
會在自己的花費上,判斷是否有各個調料,有的話將調料的價格加上去
這會出現這樣的問題:
- 調料價錢的改變會使我們改變現有代碼
- 一旦出現新的調料,就要加上新的方法,並改變超類種的
cost()
方法 - 比如,對於熱咖啡來說,加”冰“這個調料是完全沒必要的,但是熱咖啡還是繼承了
hasIce()
這個方法 - ……
認識裝飾者模式
- 設計原則:類應該對擴展開放,對修改關閉
我們瞭解到,利用繼承無法完全解決問題,在星巴茲我們遇到的問題有:類爆炸、設計死板、基類加入新功能並不適用於所有子類
我們可以這樣做:以飲料爲主體,之後在運行過程中用調料來“裝飾“(decorate
)它,比如顧客想要摩卡咖啡
- 拿一個深焙咖啡(
DarkRoast
)對象 - 加入摩卡(
Mocha
),我們稱之爲以摩卡對象裝飾它 - 調用
cost()
方法,並依賴委託(delegrate)將調料的錢加上去
以裝飾器構造飲料訂單
- 首先準備一個深焙咖啡(
DarkRoast
)對象
- 顧客想要摩卡(
Mocha
),我們建立一個Mocha
對象,用Mocha
對象把DarkRoast
對象包裝(wrap)起來Mocha
對象就是一個裝飾者,它的父類型與被包裝類型相同,所以Mocha
對象也繼承Beverage
類Mocha
對象由於繼承Beverage
,也有cost()
,內部會有調用被包裝類DarkRoast
的cost()
的代碼,也有加上摩卡價錢的代碼
- 如果顧客還想要牛奶(
Milk
),就在外面包一層牛奶- 同理,
Milk
類是裝飾器,繼承被裝飾者的父類Beverage
Milk
類內部會有調用被包裝類Mocha
的cost()
的代碼,也有加上牛奶價錢的代碼
- 同理,
- 這樣我們計算總價格時,調用
Milk
的cost()
,會自動向下調用,調用之後返回加上本層價格的錢
目前我們知道了:
- 裝飾者和被裝飾對象具有相同的超類,也正因如此,在任何需要原始對象的場合,可以使用被裝飾過的對象來替換
- 可以用一個或多個裝飾着包裝對象
- 裝飾者可以在所委託被裝飾者的行爲之前/之後加上自己的行爲,達到特定目的(後面我們會討論與代理模式的不同,可以直接通過目錄跳轉到)
- 對象可以在任何時候背修飾,所以可以在運行時動態的、不限量的用你喜歡的裝飾器修飾對象
定義裝飾者模式
裝飾者模式:動態的將責任附加到對象上,若要擴展功能,裝飾着提供了比繼承更有彈性的替代方案
所有具體裝飾者中,會有一個基類類型Component
的引用,這個引用指向被裝飾對象,裝飾者通過這個引用來調用被裝飾對象的成員
修改星巴茲
新的設計
新的星巴茲使用了裝飾者模式之後就變成了下圖的結構
加入了CondimentDecorator
類其實是爲了使繼承鏈更加清晰,所有裝飾器繼承自CondimentDecorator
,而不是直接繼承Beverage
類,這樣裝飾器就不與其他基本咖啡種類混在一起,繼承鏈十分清晰
考慮到不同的調料有不同 的特點,自然就會有不同的標語,我們在CondimentDecorator
使用抽象類覆蓋了父類的getDescription()
,準備在父類基礎標語上加上修飾詞,比如“濃郁的”、“香甜的”
星巴茲的代碼
基類代碼
首先是所有咖啡的基類,同時也是裝飾器的基類Beverage
abstract class Beverage{
// 咖啡店的宣傳標語
String description = "Unknown Beverage";
public void getDesString() {
System.out.println(this.description);
}
// 咖啡的花銷,子類自己實現
public abstract void cost();
}
之後是調料(Condiment
)類,也就是所有具體裝飾者(具體調料)的父類
public abstract class CondimentDecorator extends Beverage{
public abstract String getDescription();
}
具體飲料類代碼
這裏就只寫一個作爲示例
public class Espresso extends Beverage {
// 這種咖啡的描述,因爲在裝飾器調用鏈的最內層調用的,所以前面的裝飾器可以在這個咖啡的描述之前,加上很多的形容詞
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99;
}
}
對於這種咖啡的描述,因爲在裝飾器調用鏈的最內層調用的,所以前面的裝飾器可以在這個咖啡的描述之前,加上很多的形容詞
具體調料代碼
public class Mocha extends CondimentDecorator{
// 這個引用指向被裝飾對象,裝飾者通過這個引用來調用被裝飾對象的成員
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
// beverage.getDescription()會一層一層的向下調用,最底下一層爲具體咖啡的描述
return beverage.getDescription() + "加了摩卡";
}
public double cost() {
return 0.2 + beverage.cost();
}
}
使用裝飾者:供應咖啡
public static void main(String[] args) throws IOException{
Beverage darkRoast = new DarkRoast(); // 拿到一杯簡單的深焙咖啡
System.out.println(darkRoast.getDescription); // 打印結果爲深焙咖啡的原本描述,假設爲“深焙咖啡”
darkRoast = new Mocha(darkRoast); // 摩卡裝飾器裝飾深焙咖啡,向深焙咖啡中加入摩卡
System.out.println(darkRoast.getDescription); // 打印結果:"深焙咖啡加摩卡"
}
JAVA中的裝飾器
JAVA中的IO很多使用了裝飾器,比如LineNumberInputStream
就是一個修飾器,它添加了計算行數的能力
這裏我們就不詳細講解了
裝飾器模式的黑暗面
- 裝飾器模式由於不斷創建新的裝飾器對象,會導致產生很多的小對象,增加代碼的複雜度,之後的工廠模式和生成器模式會對這個問題有所解決
- 如果某些代碼依賴於具體類型
類型1
,需要那些只有類型1
而它的父類類型0
沒有的屬性和方法,但是修飾器卻繼承於類型0
,這時如果我們使用裝飾器替代原類型就會出現問題
回顧
OO原則
- 封裝變化
- 多用組合,少用繼承
- 針對接口編程,不針對實現編程
- 爲交互對象之間的鬆耦合設計而努力
- 對擴展開放,對修改關閉
裝飾器模式
裝飾器模式:動態的將責任附加到對象上,想要擴展功能,裝飾者提供有利於繼承的另一種選擇
要點
- 繼承屬於擴展形式之一,但不見得是達到彈性設計的最佳方式
- 我們的設計中,應允許行爲可以被擴展,無需修改現有的代碼
- 組合和委託可用於運行時動態添加新行爲
- 除了繼承,裝飾者模式可以讓我們擴展行爲
- 裝飾者模式意味着一羣裝飾者類。這些類用來包裝具體組件
- 裝飾者類與被裝飾者組件有相同的類型(通過繼承或接口)
- 裝飾者可以在被裝飾者前後加上自己行爲,甚至可以取代
- 可以用無數個裝飾者包裝一個組件
- 裝飾者一般對組件的客戶透明,除非客戶程序依賴於具體的組件類型
- 裝飾者會導致設計中出現許多小對象,如果過度使用會使程序變複雜
補充知識:裝飾者模式與代理模式的區別2
-
對裝飾器模式來說,裝飾者(Decorator)和被裝飾者(Decoratee)都實現一個接口。對代理模式來說,代理類(Proxy Class)和真實處理的類(Real Class)都實現同一個接口。此外,不論我們使用哪一個模式,都可以很容易地在真實對象的方法前面或者後面加上自定義的方法。
-
在上面的例子中,裝飾器模式是使用的調用者從外部傳入的被裝飾對象(coffee),調用者只想要你把他給你的對象裝飾(加強)一下。而代理模式使用的是代理對象在自己的構造方法裏面new的一個被代理的對象,不是調用者傳入的。調用者不知道你找了其他人,他也不關心這些事,只要你把事情做對了即可。
-
裝飾器模式關注於在一個對象上動態地添加方法,而代理模式關注於控制對對象的訪問。換句話說,用代理模式,代理類可以對它的客戶隱藏一個對象的具體信息。因此當使用代理模式的時候,我們常常在一個代理類中創建一個對象的實例;當使用裝飾器模式的時候,我們通常的做法是將原始對象作爲一個參數傳給裝飾器的構造器。
-
裝飾器模式和代理模式的使用場景不一樣,比如IO流使用的是裝飾者模式,可以層層增加功能。而代理模式則一般是用於增加特殊的功能,有些動態代理不支持多層嵌套。
-
代理和裝飾其實從另一個角度更容易去理解兩個模式的區別:代理更多的是強調對對象的訪問控制,比如說,訪問A對象的查詢功能時,訪問B對象的更新功能時,訪問C對象的刪除功能時,都需要判斷對象是否登陸,那麼我需要將判斷用戶是否登陸的功能抽提出來,並對A對象、B對象和C對象進行代理,使訪問它們時都需要去判斷用戶是否登陸,簡單地說就是將某個控制訪問權限應用到多個對象上;而裝飾器更多的強調給對象加強功能,比如說要給只會唱歌的A對象添加跳舞功能,添加說唱功能等,簡單地說就是將多個功能附加在一個對象上。
-
所以,代理模式注重的是對對象的某一功能的流程把控和輔助,它可以控制對象做某些事,重心是爲了借用對象的功能完成某一流程,而非對象功能如何。而裝飾模式注重的是對對象功能的擴展,不關心外界如何調用,只注重對對象功能加強,裝飾後還是對象本身。