Head First 設計模式筆記 3.裝飾者模式

zhazha_hui發表於2020-12-30


前後文
1.策略模式
2.觀察者模式

摘要

這篇部落格簡要通過一個訂單系統案例,各種調料裝飾飲品,最後計算咖啡價格。由此介紹了對擴充套件開放-對修改關閉的設計原則和裝飾者模式的基本概念,它的基礎類圖,最後用設計者模式將調味料作為裝飾者,將飲料作為被裝飾者。解決了這一問題並且實現了對應程式碼。

一杯咖啡引起的血案

叮叮叮,我們萬能的小明又接到任務了。這次甲方爸爸的要求是一個咖啡廳的訂單系統。咖啡店中,顧客可以加入各種調料,例如,蒸奶(Steam Milk),豆漿(Soy),摩卡(Mocha)或者奶泡。咖啡廳會根據加入的調料收取不同的費用。所以訂單系統必須要考慮調料價格。
還是那句話,作為一名優秀的程式設計師,小明很快想出來瞭解決方法。他將各種調料設計為例項bool型別變數,表示它們是否加上了對應的調料。這是他的基類Beverage(飲料的意思)圖。

Head First 設計模式筆記 3.裝飾者模式
圖1 基類圖
小明利用繼承,實現了以下幾個類。
Head First 設計模式筆記 3.裝飾者模式
圖2 各類咖啡圖

但是,很快小明發現,寫程式碼很容易,維護這些程式碼卻異常地困難。參考部落格

  • 調料價錢的改變會使我們改變現有程式碼
  • 一旦出現新的調料,我們就需要加上新的方法,並改變超類中的 cost() 方法
  • 以後可能會開發出新飲料。對這些飲料而言(例如:冰茶),某些調料可能並不適合,但是在這個設計方式中,Tea (茶)子類仍然將繼承那些不適合的方法,例如:hasWhip() (加奶泡)
  • 萬一顧客想要雙倍摩卡或咖啡,怎麼辦?
  • 調料價錢隨著具體飲料而改變
  • 飲料基礎價錢隨著大中小被的不同而改變

此刻,小明面臨了最重要的設計原則開放-關閉原則

類應該對擴充套件開放,對修改關閉

什麼意思呢?就是說我們的設計應該使得類容易擴充套件,在不修改現有程式碼的情況下,就可以搭配新的行為。兩個例子

  • 我們之前學過的觀察者模式,在無需修改主題的情況下,就可以增減觀察者的個數。
  • 策略模式,我們無需修改鴨子的程式碼,只需要給它注入不同的翅膀和叫聲就行了。這樣的設計具有彈性可以應對變化。

裝飾者模式

這裡小明想到了一個好辦法,就是像套娃一樣,用一層層的調料去裝飾飲料。例如顧客想要一倍加了摩卡和奶泡的烘焙咖啡。要做的就是

  1. 新建一個深培咖啡(DarkRoast)
  2. 用摩卡(Mocha)裝飾它
  3. 用奶泡(Whip)裝飾它
  4. 呼叫cost()方法,依賴委託將調料的價格加上去。
    計算價格的過程如圖所示
Head First 設計模式筆記 3.裝飾者模式
圖3 價格計算流程

這就是一個裝飾者模式的例項。

裝飾者模式動態地將責任新增到物件上,若要擴充套件功能,裝飾者提供了比繼承更有彈性的替代方案

我們可以從這個例項中發現裝飾者模式的特點

  • 裝飾者和被裝飾者有同樣的超型別
  • 可以用一個或者多個裝飾者包裝一個物件
  • 裝飾類可以在所委託的被裝飾類的行為之前(或之後),加上自己的行為,以達到特定的目的
  • 裝飾者可以在任何時候裝飾,所以可以在執行的時候動態地,任意數量的裝飾者來裝飾物件

下面是裝飾者模式的類圖

Head First 設計模式筆記 3.裝飾者模式
圖4 裝飾者模式類圖

然而,裝飾者模式同樣是有它的陰暗面的,這像套娃一樣一層套一層的會增加程式碼的複雜度,滋生很多的小類。尤其是在巢狀的裝飾者多了之後,理解和除錯程式碼都是一件麻煩事。而且當被裝飾者依賴某種型別時,引入裝飾者就可能出現狀況。

用裝飾者模式點綴咖啡

知道了裝配這模式的框架,那麼讓我們用咖啡訂單系統套一套。

Head First 設計模式筆記 3.裝飾者模式
圖4 咖啡系統訂單

後面都是程式碼,不感興趣的朋友可以直接跳到小結。

讓我們來實現Beverage類

public abstract class Beverage {
	String description = "Unknown Beverage";
  
	public String getDescription() {
		return description;
	}
 
	public abstract double cost();
}

Condiment調料類

public abstract class CondimentDecorator extends Beverage {
	Beverage beverage;
	public abstract String getDescription();
}

飲料Espresso程式碼,其他飲料HouseBlend, DarkRoast, Espresso, Decaf都類似,這裡就不照抄了

public class Espresso extends Beverage {
  
	public Espresso() {
		description = "Espresso";
	}
  
	public double cost() {
		return 1.99;
	}
}

調料Mocha程式碼,其他的調料Milk, Soy, Whip等都類似,這裡就不抄寫了。


public class Mocha extends CondimentDecorator {
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
		// 裝飾者的建構函式需要被裝飾者被賦值
	}
 
	public String getDescription() {
		return beverage.getDescription() + ", Mocha";
	}
 
	public double cost() {
		return .20 + beverage.cost();
	}
}

讓我們寫一個測試程式碼測試一下,這裡的程式碼有些類沒有實現。需要實現了才能執行

package headfirst.designpatterns.decorator.starbuzz;

public class StarbuzzCoffee {
 
	public static void main(String args[]) {
		Beverage beverage = new Espresso();
		System.out.println(beverage.getDescription() 
				+ " $" + beverage.cost());
 		//製造一個DarkRoast物件,並且裝飾上兩個摩卡,一個奶泡
 		//這樣最後就能夠返回一杯加上了兩份摩卡和一個奶泡的烘焙咖啡的價格
		Beverage beverage2 = new DarkRoast();
		beverage2 = new Mocha(beverage2);
		beverage2 = new Mocha(beverage2);
		beverage2 = new Whip(beverage2);
		System.out.println(beverage2.getDescription() 
				+ " $" + beverage2.cost());
 		
 		//返回一份加了Whip,Mocha,Soy的HouseBlend的價格
		Beverage beverage3 = new HouseBlend();
		beverage3 = new Soy(beverage3);
		beverage3 = new Mocha(beverage3);
		beverage3 = new Whip(beverage3);
		System.out.println(beverage3.getDescription() 
				+ " $" + beverage3.cost());
	}
}

假如讀者不想寫程式碼,這裡有程式碼的連結。
咖啡廳程式碼連結

小結

在這篇部落格中,我們又get到了新的設計原則:

擴充套件開放-對修改關閉

裝飾者模式被我們加入了工具箱

裝飾者模式動態地將責任新增到物件上,若要擴充套件功能,裝飾者提供了比繼承更有彈性的替代方案

裝飾者模式具有以下特點

  • 由於共屬於一個超類,裝飾者模式允許我們用無數個裝飾者裝飾元件。只要我們需要,我們可以在咖啡中加上任意調味品裝飾。
  • 裝飾類可以在所委託的被裝飾類的行為之前(或之後),加上自己的行為,以達到特定的目的。我們就是這樣計算出加上調味品的咖啡的價格的
  • 然而,過多的裝飾者會導致程式碼變得很複雜難以理解和除錯。請小心使用。

Java.io包中的典型的裝飾者模式如下。

Head First 設計模式筆記 3.裝飾者模式
圖5 典型的裝飾者模式

謝謝你的閱讀。你們的閱讀點贊是我更新最大的動力 (๑◕ܫ←๑)

相關文章