聲明:僅為個人學習總結,還請批判性查看,如有不同觀點,歡迎交流。
摘要
《Head First設計模式》第3章筆記:結合示例應用和代碼,介紹裝飾者模式,包括遇到的問題、遵循的 OO 原則、達到的效果。
目錄
- 摘要
- 1 示例應用
- 2 遇到問題
- 3 引入設計模式
- 3.1 OO 原則:開閉原則
- 3.2 完善“裝飾者”設計
- 3.3 完善“被裝飾對象”設計
- 3.4 裝飾者模式定義
- 4 示例代碼
- 4.1 Java 示例
- 4.2 C++11 示例
- 5 設計工具箱
- 5.1 OO 基礎
- 5.2 OO 原則
- 5.3 OO 模式
- 參考
1 示例應用
示例應用是星巴茲(Starbuzz)咖啡店的訂單系統。
最開始,店里只提供黑咖啡,系統類圖如下:
Beverage
(飲料),是一個抽象類,咖啡店售賣的所有飲料都繼承這個類- 定義
description
實例變量,用于保存飲料描述; - 定義
getDescription()
方法,用于獲取description
; - 聲明抽象的
cost()
方法,用于獲取飲料價格,由子類負責實現。
- 定義
Beverage
的子類:HouseBlend
(混合咖啡)、DarkRoast
(深度烘焙)、Decaf
(低咖啡因)、Espresso
(濃縮咖啡)- 實現
cost()
方法,計算具體飲料價格; - 將具體飲料描述賦值給
description
,例如“最優深度烘焙”。
- 實現
后來,咖啡店提供了牛奶等調味配料,每份調料會收取一點費用。
隨著調料和飲料的種類不斷增加,現在系統急需進行更新。下面是當前的系統類圖:
類爆炸!
調料的種類包括 Steamed Milk(蒸牛奶)、Soy(豆奶)、Mocha(摩卡,也被稱為巧克力)、Whip(奶油泡沫)等,系統為每個“飲料和調料的組合”都創建了類。
我們來近距離觀察一下 DarkRoastWithMochaAndWhip
(深焙摩卡奶泡咖啡)類的定義:
public class DarkRoastWithMochaAndWhip extends Beverage {public DarkRoastWithMochaAndWhip() {description = "Dark Roast with Mocha and Whip";}public double cost() {return 0.99 + 0.2 + 0.1; // 基礎飲料價格 + 各種調料價格}
}
訂購一杯“深焙摩卡奶泡咖啡”的方式如下:
Beverage beverage = new DarkRoastWithMochaAndWhip();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
思考題:
很明顯,星巴茲為自己制造了一個維護噩夢。請思考如下問題:【參考答案在第 20 行】
It’s pretty obvious that Starbuzz has created a maintenance nightmare for themselves. The Brain Power exercises:1. 在牛奶價格上漲時,系統需要怎樣調整? What happens when the price of milk goes up?
2. 當新增一種焦糖調料時,系統需要怎樣調整? What do they do when they add a new caramel topping?
3. 系統設計明顯違反了下面哪些設計原則?分離變與不變、針對接口編程、優先使用組合、松耦合設計(下文 “5.2.2 原則回顧” 部分有詳細些的原則介紹)Which of the design principles that we’ve covered so far are they violating? (Hint: they’re violating two of them in a big way!)書上沒有提供答案,以下是我的理解,不知和你的是否一樣。
1. 對于所有帶牛奶調料的飲料類,都需要修改其 cost() 方法;(如果價格在每個類中硬編碼)
2. 對于所有可搭配焦糖調料的飲料,都需要創建新的類;(類就是這樣爆炸的)
3. 違反的原則1)分離變與不變:變化的方面包括調料價格、調料類型、為基礎飲料添加不同的調料2)優先使用組合:系統完全采用繼承方式進行設計
2 遇到問題
為了解決“類爆炸”,系統有了第1個版本的新設計,類圖如下:
在 Beverage
類中:
- 定義
milk
、soy
、mocha
、whip
實例變量,代表是否添加相應調料; - 定義
has調料()
和set調料()
方法,用于獲取和設置調料的布爾值; cost()
不再是抽象方法:- 在超類
cost()
中,計算當前飲料實例中所有調料的價格; - 在子類
cost()
中,通過超類cost()
獲得調料價格,再加上子類的基礎飲料價格,就可以計算出總價格。
- 在超類
超類 Beverage
的 cost()
方法實現如下:
public class Beverage {public double cost() {double condimentCost = 0.0; // 調料(condiment)總價格if (hasMilk()) { condimentCost += 0.10; }if (hasSoy()) { condimentCost += 0.15; }if (hasMocha()) { condimentCost += 0.20; }if (hasWhip()) { condimentCost += 0.10; }return condimentCost;}
}
子類 DarkRoast
的 cost()
方法實現如下:
public class DarkRoast extends Beverage {public double cost() {return 0.99 + super.cost(); // 基礎飲料價格 + 所有調料價格}
}
訂購一杯“深焙摩卡奶泡咖啡”的方式如下:
Beverage beverage = new DarkRoast();
beverage.setMocha(true);
beverage.setWhip(true);
System.out.println(beverage.getDescription() + " $" + beverage.cost());
在第1版的新設計中,一共只有5個類,已經解決了“類爆炸”的問題。
思考題:
哪些需求或其它因素的改變會影響這個設計?(多選)【答案在第 20 行】
What requirements or other factors might change that will impact this design? (Choose all that apply.)A. 任意一種調料的價格改變,都需要修改超類代碼。Price changes for condiments will force us to alter existing code.
B. 每次增加新的調料時,都需要在超類中添加新的方法,并修改其 cost() 方法。New condiments will force us to add new methods and alter the cost method in the superclass.
C. 未來可能會有某些飲料,并不適合某些調料,但是在這些飲料子類中,仍然要繼承超類中的“那些不適合調料的”相關方法。We may have new beverages. For some of these beverages (iced tea?), the condiments may not be appropriate, yet the Tea subclass will still inherit methods like hasWhip().
D. 如果顧客想添加雙份摩卡,該怎么辦?What if a customer wants a double mocha?答案:A B C D
3 引入設計模式
3.1 OO 原則:開閉原則
設計原則(Design Principle)
類應該對擴展開放,對修改關閉。
Classes should be open for extension, but closed for modification.
- 對修改關閉
- 抱歉,我們的類已經關閉,不能被修改。
Sorry, our classes must remain closed to modification. - 我們花了許多時間得到了正確的代碼,解決了所有的bug,所以不能讓你修改現有代碼。如果你不喜歡,可以找經理談。
We spent a lot of time getting this code correct and bug free, so we can’t let you alter the existing code. If you don’t like it, you can speak to the manager.
- 抱歉,我們的類已經關閉,不能被修改。
- 對擴展開放
- 歡迎擴展我們的類,加入任何你想要的新行為。
Feel free to extend our classes with any new behavior you like. - 如果你的要求或需求有所改變(我們知道這一定會發生),那就開始吧,創建你自己的擴展。
If your needs or requirements change (and we know they will), just go ahead and make your own extensions.
- 歡迎擴展我們的類,加入任何你想要的新行為。
“開閉原則”的目標是允許類能夠輕松地擴展,在不修改現有代碼的情況下,就可以加入新的行為。
Our goal is to allow classes to be easily extended to incorporate new behavior without modifying existing code.
這樣的設計具有彈性,可以應對改變;并且足夠靈活,能夠添加新的功能以滿足不斷變化的需求。
Designs that are resilient to change and flexible enough to take on new functionality to meet changing requirements.
結合“深焙摩卡奶泡咖啡”,我們來了解一種符合“開閉原則”的設計方式:
- 定義
DarkRoast
和DarkRoastWithMocha
兩個類(暫時不考慮其它類) DarkRoastWithMocha
組合DarkRoast
,即 HAS-A(有一個)DarkRoast
類型對象wrappedObj
- 將
DarkRoastWithMocha
叫做 “裝飾者”; - 將
DarkRoast
類型的實例變量wrappedObj
叫做 “被裝飾對象”; - “裝飾者” 可以將行為委托給 “被裝飾對象”,并且在 “被裝飾對象” 行為的基礎上,“裝飾” 新的行為。
- 將
DarkRoastWithMocha
繼承DarkRoast
,即 IS-A(是一個)DarkRoast
類型- “裝飾者” 和 “被裝飾對象” 具有相同的類型;
- 當 “裝飾者” 是 “被裝飾對象” 類型(
DarkRoast
)變量時:- 使用變量的用戶并不知道 “裝飾者” 類型的存在(“裝飾者” 對用戶透明);
- 用戶可以像使用 “被裝飾對象” 一樣,使用 “裝飾者”;
- 用戶可以將 “裝飾者” 當做 “被裝飾對象” 再次進行 “裝飾” ,即 “遞歸裝飾”。
這樣,就可以在不改變 “被裝飾對象” 的情況下,通過 “裝飾者” 對 “被裝飾對象” 進行擴展。即 “對修改關閉,對擴展開放”。
下面結合示例代碼,了解具體的實現過程。
“被裝飾對象”類的定義:
public class DarkRoast {String description;public DarkRoast() { description = "Dark Roast"; }public String getDescription() { return description; }public double cost() { return 0.99; }
}
“裝飾者”類的定義:
public class DarkRoastWithMocha extends DarkRoast {DarkRoast wrappedObj; // “被裝飾對象”// 創建“裝飾者”時,需要指定“被裝飾對象”public DarkRoastWithMocha(DarkRoast darkRoast) { wrappedObj = darkRoast; }// “裝飾者”將行為委托給“被裝飾對象”,并且在“被裝飾對象”行為的基礎上,“裝飾”新的行為public String getDescription() {return wrappedObj.getDescription() + ", Mocha";}public double cost() {return wrappedObj.cost() + 0.2;}
}
訂購一杯“深焙摩卡奶泡咖啡”的方式如下:
// 生成一份“深焙”
DarkRoast darkRoast = new DarkRoast();
System.out.println(darkRoast.getDescription() // "Dark Roast"+ " $" + darkRoast.cost()); // 0.99// 為“深焙”(被裝飾對象)添加摩卡裝飾,生成“深焙摩卡”(既是裝飾者,又可以作為被裝飾對象)
darkRoast = new DarkRoastWithMocha(darkRoast);
System.out.println(darkRoast.getDescription() // "Dark Roast, Mocha"+ " $" + darkRoast.cost()); // 0.99 + 0.2 = 1.19// 如果有奶泡裝飾者 DarkRoastWithWhip 類,我們就可以
// 為“深焙摩卡”(被裝飾對象)添加奶泡裝飾,生成“深焙摩卡奶泡”(遞歸裝飾)
// darkRoast = new DarkRoastWithWhip(darkRoast);
// System.out.println(darkRoast.getDescription() // "Dark Roast, Mocha, Whip"
// + " $" + darkRoast.cost()); // 1.19 + 0.1 = 1.29// 目前還沒有奶泡裝飾者,但是我們可以
// 為“深焙摩卡”(被裝飾對象)繼續添加摩卡裝飾,生成“深焙雙摩卡”(重復裝飾)
darkRoast = new DarkRoastWithMocha(darkRoast);
System.out.println(darkRoast.getDescription() // "Dark Roast, Mocha, Mocha"+ " $" + darkRoast.cost()); // 1.19 + 0.2 = 1.39
上述示例在不改變 DarkRoast
對象的情況下,通過 DarkRoastWithMocha
,以動態(運行時)、透明(類型不變)的方式,擴展了對象的 getDescription()
和 cost()
行為。
3.2 完善“裝飾者”設計
在訂單系統中,要采用上述類圖結構,我們還需要進行一些完善。
首先,類圖中不能只有摩卡,還要添加其它調料:
- 由于調料的價格和類型可能會發生改變,所以需要對每種調料進行封裝;
- 遵循“針對接口編程”原則,需要為所有調料定義統一的接口。
完善“裝飾者”設計后的類圖如下:
關于 “裝飾者” 部分:
- 調料裝飾者抽象類
CondimentDecorator
- 繼承
DarkRoast
,并引用DarkRoast
類型的 “被裝飾對象”wrappedObj
; - 聲明抽象的
getDescription()
和cost()
方法,由子類負責實現。
- 繼承
- 調料裝飾者類
Milk
、Mocha
、Soy
、Whip
- 繼承
CondimentDecorator
,實現getDescription()
和cost()
方法,
在方法內部,委托 “被裝飾對象” 執行行為,并在 “被裝飾對象” 行為的基礎上,“裝飾” 新的行為(補充描述,增加價格)。
- 繼承
3.3 完善“被裝飾對象”設計
接下來,類圖中也不能只有深度烘焙,還要添加其它飲料:
和調料部分的設計一樣,也將每種飲料封裝在各自的類中,并定義統一的接口。
現在,我們獲得了完整的、更新后的系統類圖:
關于 “被裝飾對象” 部分:
- 飲料抽象類
Beverage
- 定義
getDescription()
方法,用于獲取飲料描述description
; - 聲明抽象的
cost()
方法,由子類負責實現,包括飲料子類和調料子類。
- 定義
- 飲料具體類
HouseBlend
、DarkRoast
、Decaf
、Espresso
- 繼承
Beverage
,為description
賦值,實現cost()
方法。
- 繼承
采用最新的設計之后,第1版設計 存在的問題 已經得到解決,系統 可以應對改變并易于擴展。
3.4 裝飾者模式定義
剛剛我們使用裝飾者模式重新設計了咖啡店的訂單系統,裝飾者模式的正式定義如下:
裝飾者模式(Decorator Pattern)
動態地給一個對象添加一些額外的職責。就增加功能來說,裝飾者模式相比生成子類更為靈活。
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
增加功能的方式:生成子類 vs 裝飾者模式
序號 | 生成子類 | 裝飾者模式 | 裝飾者模式【優缺點】 |
---|---|---|---|
1 | 給整個類添加功能 | 給某個對象添加功能 | 優點:讓“被裝飾對象”的類保持簡單; 缺點:系統會產生許多看上去相似的“裝飾者”類,不便于學習和調試; 缺點:在使用時,不僅要實例化“被裝飾對象”,還要實例化各種需要的“裝飾者” |
2 | 編譯時靜態添加功能 | 運行時動態添加功能 | 優點:可以在運行時動態添加功能、組合多種不同功能、重復添加一種功能 |
3 | 直接執行功能 | 委托“被裝飾對象” 執行功能 | 優點:因為“裝飾者”與“被裝飾對象”具有相同的“超類型”,所以可以對用戶透明; 缺點:不適用于依賴具體類型的代碼,例如“被裝飾對象”定義了某個方法 f() ,而“超類型”并沒有定義 f() ,則不能通過“裝飾者”執行 f() |
將訂單系統中的類圖抽象化,就可以得到設計模式中的類圖:
Component
抽象類- 為 “被裝飾對象” 定義統一的接口,聲明抽象方法
operation()
。
- 為 “被裝飾對象” 定義統一的接口,聲明抽象方法
ConcreteComponent
“被裝飾對象” 類- 繼承
Component
,實現operation()
方法; - “被裝飾對象” 可以單獨使用;也可以被 “裝飾者” 加上新行為(裝飾,wrapped)后再使用。
- 繼承
Decorator
“裝飾者” 抽象類- 繼承
Component
,即 IS-A(是一個)Component
,和 “被裝飾對象” 具有相同的超類型;- 繼承的目的是:達到類型匹配(這樣 “裝飾者” 可以充當 “被裝飾對象”),而不是復用超類的行為。
- 組合
Component
,即 HAS-A(有一個)Component
類型的 “被裝飾對象”wrappedObj
;- 組合的目的是:將行為委托給 “被裝飾對象”。
- 實現
operation()
方法,將行為委托給 “被裝飾對象”wrappedObj
;
注:由于當前 mermaid 類圖不支持 note,所以方法(method)的返回類型都被用于作為注釋,如 CallOperationOfWrappedObj
- 繼承
ConcreteDecoratorA
“裝飾者” 具體類- 繼承
Decorator
,和 “被裝飾對象” 具有相同的超類型; - 定義
addedBehavior()
方法,實現向 “被裝飾對象” 添加的新行為; - 實現
operation()
方法,在委托 “被裝飾對象” 執行operation()
的基礎上,附加addedBehavior()
行為;- 對于附加行為,即可以放在 “被裝飾對象” 行為的前面,也可以放在其后面。
- 繼承
ConcreteDecoratorB
“裝飾者” 具體類- 繼承
Decorator
,和 “被裝飾對象” 具有相同的超類型; - 定義
newState
實例變量和newBehavior()
方法- 為 “當前類的特定功能” 添加新狀態和新行為;
- 例如,“被裝飾對象” 是文本視圖
TextView
,“裝飾者” 是滾動條ScrollDecorator
,
為了實現視圖滾動功能,就會在ScrollDecorator
中定義滾動狀態scrollPosition
和滾動行為ScrollTo()
。
- 實現
operation()
方法,在委托 “被裝飾對象” 執行operation()
的基礎上,根據需要附加額外行為。
- 繼承
注:在設計模式中,重要的是模式的核心思想;在實際設計時,選擇定義為接口還是抽象類,以及是否提供默認的方法實現等,可以根據具體的情境來決定。
延伸閱讀:《設計模式:可復用面向對象軟件的基礎》 4.4 Decorator(裝飾)— 對象結構型模式 [P132-139]
4 示例代碼
4.1 Java 示例
抽象飲料和調料類定義:
// Beverage.java
public abstract class Beverage {String description = "Unknown Beverage";public String getDescription() { return description;}public abstract double cost();
}// CondimentDecorator.java
public abstract class CondimentDecorator extends Beverage {Beverage beverage;public abstract String getDescription();
}
具體飲料類定義:
// DarkRoast.java
public class DarkRoast extends Beverage {public DarkRoast() { description = "Dark Roast Coffee"; }public double cost() { return .99; }
}
具體調料類定義:
// Mocha.java
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();}
}// Whip.java
public class Whip extends CondimentDecorator {public Whip(Beverage beverage) {this.beverage = beverage;}public String getDescription() {return beverage.getDescription() + ", Whip";}public double cost() {return .10 + beverage.cost();}
}
測試代碼:
// StarbuzzCoffee.java
public class StarbuzzCoffee {public static void main(String args[]) {Beverage beverage = new DarkRoast();System.out.println(beverage.getDescription() + " $" + beverage.cost());beverage = new Mocha(beverage);System.out.println(beverage.getDescription() + " $" + beverage.cost());beverage = new Mocha(beverage);beverage = new Whip(beverage);System.out.println(beverage.getDescription() + " $" + beverage.cost());}
}
4.2 C++11 示例
抽象飲料和調料類定義:
struct Beverage {virtual ~Beverage() = default;virtual std::string getDescription() const { return description; }virtual double cost() = 0;protected:std::string description = "Unknown Beverage";
};struct CondimentDecorator : public Beverage {virtual ~CondimentDecorator() = default;virtual std::string getDescription() const override { return beverage->getDescription(); }virtual double cost() override { return beverage->cost(); }protected:CondimentDecorator(std::shared_ptr<Beverage> beverage) : beverage(beverage) {}std::shared_ptr<Beverage> beverage;
};
具體飲料類定義:
struct DarkRoast : public Beverage {DarkRoast() { description = "Dark Roast Coffee"; }double cost() override { return .99; }
};
具體調料類定義:
struct Mocha : public CondimentDecorator {Mocha(std::shared_ptr<Beverage> beverage) : CondimentDecorator(beverage) {}std::string getDescription() const override {return CondimentDecorator::getDescription() + ", Mocha";}double cost() override { return CondimentDecorator::cost() + .20; }
};struct Whip : public CondimentDecorator {Whip(std::shared_ptr<Beverage> beverage) : CondimentDecorator(beverage) {}std::string getDescription() const override {return CondimentDecorator::getDescription() + ", Whip";}double cost() override { return CondimentDecorator::cost() + .10; }
};
測試代碼:
#include <iostream>
#include <memory>
#include <string>// 在這里添加相關接口和類的定義int main() {std::shared_ptr<Beverage> beverage = std::make_shared<DarkRoast>();std::cout << beverage->getDescription() << " $" << beverage->cost() << "\n";beverage = std::make_shared<Mocha>(beverage);std::cout << beverage->getDescription() << " $" << beverage->cost() << "\n";beverage = std::make_shared<Mocha>(beverage);beverage = std::make_shared<Whip>(beverage);std::cout << beverage->getDescription() << " $" << beverage->cost() << "\n";
}
5 設計工具箱
5.1 OO 基礎
OO 基礎回顧
- 抽象(Abstraction)
- 封裝(Encapsulation)
- 繼承(Inheritance)
- 多態(Polymorphism)
5.2 OO 原則
5.2.1 新原則
類應該對擴展開放,對修改關閉。
Classes should be open for extension, but closed for modification.
5.2.2 原則回顧
- 封裝變化。
Encapsulate what varies. - 針對接口編程,而不是針對實現編程。
Program to interfaces, not implementations. - 優先使用組合,而不是繼承。
Favor composition over inheritance. - 盡量做到交互對象之間的松耦合設計。
Strive for loosely coupled designs between objects that interact.
5.3 OO 模式
5.3.1 新模式
裝飾者模式(Decorator Pattern)
- 裝飾者模式動態地給一個對象添加一些額外的職責。
The Decorator Pattern attaches additional responsibilities to an object dynamically. - 就增加功能來說,裝飾者模式相比生成子類更為靈活。
Decorators provide a flexible alternative to subclassing for extending functionality.
5.3.2 模式回顧
- 策略模式(Strategy Pattern)
- 定義一個算法家族,把其中的算法分別封裝起來,使得它們之間可以互相替換。
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. - 讓算法的變化獨立于使用算法的客戶。
Strategy lets the algorithm vary independently from clients that use it.
- 定義一個算法家族,把其中的算法分別封裝起來,使得它們之間可以互相替換。
- 觀察者模式(Observer Pattern)
- 定義對象之間的一對多依賴,
The Observer Pattern defines a one-to-many dependency between objects - 這樣一來,當一個對象改變狀態時,它的所有依賴者都會被通知并自動更新。
so that when one object changes state, all of its dependents are notified and updated automatically.
- 定義對象之間的一對多依賴,
參考
- [美]弗里曼、羅布森著,UMLChina譯.Head First設計模式.中國電力出版社.2022.2
- [美]伽瑪等著,李英軍等譯.設計模式:可復用面向對象軟件的基礎.機械工業出版社.2019.3
- wickedlysmart: Head First設計模式 Java 源碼
Hi, I’m the ENDing, nice to meet you here! Hope this article has been helpful.