設計模式:裝飾者模式

運行時擴展,比編譯時繼承威力更大

裝飾對象,給愛用繼承的人一個全新的設計眼界

星巴茲(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)它,比如顧客想要摩卡咖啡

  1. 拿一個深焙咖啡(DarkRoast)對象
  2. 加入摩卡(Mocha),我們稱之爲以摩卡對象裝飾它
  3. 調用cost()方法,並依賴委託(delegrate)將調料的錢加上去

以裝飾器構造飲料訂單

  1. 首先準備一個深焙咖啡(DarkRoast)對象
    在這裏插入圖片描述
  2. 顧客想要摩卡(Mocha),我們建立一個Mocha對象,用Mocha對象把DarkRoast對象包裝(wrap)起來
    • Mocha對象就是一個裝飾者,它的父類型與被包裝類型相同,所以Mocha對象也繼承Beverage
    • Mocha對象由於繼承Beverage,也有cost(),內部會有調用被包裝類DarkRoastcost()的代碼,也有加上摩卡價錢的代碼
      在這裏插入圖片描述
  3. 如果顧客還想要牛奶(Milk),就在外面包一層牛奶
    • 同理,Milk類是裝飾器,繼承被裝飾者的父類Beverage
    • Milk類內部會有調用被包裝類Mochacost()的代碼,也有加上牛奶價錢的代碼
      在這裏插入圖片描述
  4. 這樣我們計算總價格時,調用Milkcost(),會自動向下調用,調用之後返回加上本層價格的錢
    在這裏插入圖片描述
    目前我們知道了:
  • 裝飾者和被裝飾對象具有相同的超類,也正因如此,在任何需要原始對象的場合,可以使用被裝飾過的對象來替換
  • 可以用一個或多個裝飾着包裝對象
  • 裝飾者可以在所委託被裝飾者的行爲之前/之後加上自己的行爲,達到特定目的(後面我們會討論與代理模式的不同,可以直接通過目錄跳轉到)
  • 對象可以在任何時候背修飾,所以可以在運行時動態的、不限量的用你喜歡的裝飾器修飾對象

定義裝飾者模式

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

所有具體裝飾者中,會有一個基類類型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對象添加跳舞功能,添加說唱功能等,簡單地說就是將多個功能附加在一個對象上。

  • 所以,代理模式注重的是對對象的某一功能的流程把控和輔助,它可以控制對象做某些事,重心是爲了借用對象的功能完成某一流程,而非對象功能如何。而裝飾模式注重的是對對象功能的擴展,不關心外界如何調用,只注重對對象功能加強,裝飾後還是對象本身。


  1. 參考:《Head First 設計模式》 ↩︎

  2. 本段來源:https://www.cnblogs.com/yanggb/p/10952843.html ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章