公號:碼農充電站pro
主頁:https://codeshellme.github.io
今天來介紹裝飾者模式(Decorator Design Pattern)。
假設我們需要給一家火鍋店設計一套結賬系統,也就是統計顧客消費的總價格。怎樣才能設計出一個好的系統呢?
1,結賬系統需求分析
既然要設計一個結賬系統,當然需要知道火鍋店都有哪些食品及食品的價格,假如我們從火鍋店老闆那裡得到以下食品清單:
- 鍋底類:
- 清湯鍋底:5 元
- 麻辣鍋底:7 元
- 其它
- 配菜類:
- 青菜:3 元
- 羊肉:6 元
- 其它
- 飲料類:
- 可樂:2 元
- 其它
可以看到,食品共有三大類,分別是:鍋底類,配菜類和飲料類,每個大類下邊都有很多具體的食品。
為了設計出一個可維護,可擴充套件,有彈性的系統,應該怎樣設計呢?
我們可以這樣看待食品之間的關係,將鍋底類看作主品,所有其它的都為副品,也就是附加在主品之上的食品。
副品以主品為中心,圍繞在主品周圍,包裹著主品,一層層的往外擴充套件。
如下圖所表達的一樣:
2,裝飾者模式
像這種,需要在原來(主品)的基礎上,附加其它的東西(副品),這樣的業務場景都可以使用裝飾者模式。
裝飾者模式的定義為:動態的給一個物件新增其它功能。從擴充套件性來說,這種方式比繼承更有彈性,更加靈活,可作為替代繼承的方案。
裝飾者模式的優點在於,它能夠更靈活的,動態的給物件新增其它功能,而不需要修改任何現有的底層程式碼。也就是不需要通過修改程式碼,而是通過擴充套件程式碼,來完成新的業務需求。
這就非常符合我們所說的設計原則中的開閉原則:對擴充套件開放,對修改關閉。也就是儘量不要修改原有程式碼,而是通過擴充套件程式碼來完成任務。這樣做的好處是可以減少對原有系統的修改,從而減少引入 bug 的風險。
裝飾者模式的類圖如下:
ConcreteComponent 為被裝飾者,Decorator 是所有裝飾者的超類。
裝飾者和被裝飾者有著共同的超型別,這一點很重要,因為裝飾者必須能夠取代被裝飾者。這樣,裝飾者才能在被裝飾者的基礎上,加上自己的行為,以增強被裝飾者的能力。
一個被裝飾者可以被多個裝飾者依次包裝,這個包裝行為是動態的,不限次數的。
3,實現結賬系統
那麼根據裝飾者模式的類圖,我們可以設計出火鍋店結賬系統的類圖,如下:
火鍋的鍋底作為被裝飾者,配菜和飲料作為裝飾者。
每個類都有兩個方法:
- describe:返回當前火鍋的描述。
- cost:返回當前火鍋的價格。
首先編寫 HotPot
類:
class HotPot {
protected String desc = "HotPot";
protected double price = 0;
public String description() {
return desc;
}
public double cost() {
return price;
}
public void printMenu() {
System.out.println("選單:" + description() + " 消費總價:" + cost());
}
}
HotPot
類中有兩個屬性 desc
和 price
,還有三個方法 description
,cost
和 printMenu
。printMenu
用於輸出選單和消費總價。
再編寫 SideDish
類:
class SideDish extends HotPot {
protected HotPot hotpot;
public double cost() {
return hotpot.cost() + price;
};
public String description() {
return hotpot.description() +" + "+ desc;
};
}
SideDish
繼承了 HotPot
,新增了自己的屬性 hotpot
,並且重寫了兩個方法 description
和 cost
。
注意:SideDish
類對兩個方法 description
和 cost
進行了重寫,這非常重要,這體現出了裝飾者與被裝飾者的區別,裝飾者能在被裝飾者的基礎上附加自己的行為,原因就在這裡。
編寫兩個鍋底類:
class SoupPot extends HotPot {
public SoupPot() {
desc = "Soup";
price = 5;
}
}
class SpicyPot extends HotPot {
public SpicyPot() {
desc = "Spicy";
price = 7;
}
}
這兩個類都繼承HotPot
,並分別在構造方法中設定自己的 desc
和 price
。
再編寫三個配菜類:
class VegetablesDish extends SideDish {
public VegetablesDish(HotPot hotpot) {
this.hotpot = hotpot;
desc = "Vegetables";
price = 3;
}
}
class MuttonDish extends SideDish {
public MuttonDish(HotPot hotpot) {
this.hotpot = hotpot;
desc = "Mutton";
price = 6;
}
}
class ColaDish extends SideDish {
public ColaDish(HotPot hotpot) {
this.hotpot = hotpot;
desc = "Cola";
price = 2;
}
}
這三個類都繼承 SideDish
,並分別在構造方法中設定自己的hotpot
,desc
和 price
。
4,測試結賬系統
用如下程式碼來測試:
// 只有一份清湯鍋底
HotPot hotpot = new SoupPot(); // 被裝飾者不需裝飾者包裝也可以使用
hotpot.printMenu();
// 清湯鍋底 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();
// 清湯鍋底 + 蔬菜 + 羊肉
hotpot = new MuttonDish(hotpot);
hotpot.printMenu();
// 清湯鍋底 + 蔬菜 + 羊肉 + 可樂
hotpot = new ColaDish(hotpot);
hotpot.printMenu();
// 清湯鍋底 + 蔬菜 + 羊肉 + 可樂 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();
輸出如下:
選單:Soup 消費總價:5.0
選單:Soup + Vegetables 消費總價:8.0
選單:Soup + Vegetables + Mutton 消費總價:14.0
選單:Soup + Vegetables + Mutton + Cola 消費總價:16.0
選單:Soup + Vegetables + Mutton + Cola + Vegetables 消費總價:19.0
計算總價時,會從最外層的裝飾者,朝著被裝飾者的方向,依次呼叫每一層的 cost
方法,直到被裝飾者為止。
然後再朝著最外層裝飾者的方向,依次計算出每一層的價格,最後得出的價格就是消費總價。
計算過程如下圖所示:
我將完整的裝飾者模式程式碼放在了這裡,供大家參考。
5,裝飾者模式的使用場景
裝飾者模式主要用於,在不修改原有類的前提下,動態的修改原有類的功能。
Java JDK 中大量使用了裝飾者模式,尤其是 Java I/O 框架。
Java IO 框架的繼承關係如下:
可以看到,Java IO 框架包含了非常多的類,這對初學者並不是很友好,很難弄明白每個類的作用是什麼,也不容易瞭解設計者的意圖。
Java IO 主要分為位元組流和字元流兩大類。我們以 InputStream 為例,畫出其類圖結構,如下:
從該圖能夠看出,Java IO 與我們上文中的裝飾者模式的類圖基本一模一樣,所以 Java IO 其實就是使用了裝飾者模式,明白了這一點,再使用它就非常方便了。
6,裝飾者模式的缺點
裝飾者模式有一個比較明顯的缺點,從上文中你也許已經發現了,就是它會引入非常多的小類,這樣會讓使用者弄不明白類之間的關係。
當了解了裝飾者的原理,也就比較容易使用了。
7,總結
從裝飾者模式中,能充分的看到開閉原則的使用。利用裝飾者模式,可以讓我們在不修改原有程式碼的情況下,擴充套件原有類的功能。但是也不能過度使用它,因為容易引入非常多的小類。
(本節完。)
推薦閱讀:
歡迎關注作者公眾號,獲取更多技術乾貨。