本文參照《Head First 設(shè)計模式》帖鸦,轉(zhuǎn)載請注明出處
對于整個系列咱筛,我們按照這本書的設(shè)計邏輯,使用情景分析的方式來描述嫌佑,并且穿插使用一些問題豆茫,總結(jié)的方式來講述侨歉。并且所有的開發(fā)源碼,都會托管到github上揩魂。
項目地址:https://github.com/jixiang52002/HFDesignPattern
回顧上一篇文章講解了設(shè)計模式中常用的一種模式------觀察者模式幽邓。并結(jié)合氣象站設(shè)計進行實戰(zhàn)解析,并且從自己設(shè)計到JAVA自帶設(shè)計模式做了講解火脉。想要了解的朋友可以回去回看一下牵舵。
Head First 設(shè)計模式(2)---觀察者(Observer)模式
本章我們會繼續(xù)前面的話題,有關(guān)典型的繼承濫用問題倦挂。這一章會講解如何使用對象組合的方式畸颅,如何在運行時候做裝飾類。在熟悉裝飾技巧后方援,我們能夠在原本不修改任何底層的代碼没炒,卻可以給原有對象賦予新的職能。你會說犯戏,這不就是“裝飾者模式”送火。沒錯,接下來就是裝飾者模式的ShowTime時間笛丙。
1.前言
歡迎來到星巴茲咖啡漾脂,該公司是世界上以擴張速度最快而聞名的咖啡連鎖店。但是最近這家著名的咖啡公司遇到一個巨大的問題胚鸯,因為擴展速度太快了骨稿,他們準(zhǔn)備更新訂單系統(tǒng),以合乎他們的飲料供應(yīng)需求姜钳。
他們本來的設(shè)計方式如下:
然后客戶購買咖啡時坦冠,可以要求在其中加入任何調(diào)料,例如:奶茶哥桥,牛奶辙浑,豆?jié){。星巴茲根據(jù)業(yè)務(wù)需求會計算相應(yīng)的費用拟糕。這就要求訂單系統(tǒng)必須考慮到這些調(diào)料的部分判呕。
然后我們就看到他們的第一個嘗試設(shè)計:
是不是有一種犯了密集恐懼癥的感覺,整完全就是“類爆炸”送滞。
那么我們分析一下侠草,這種設(shè)計方式違反了什么設(shè)計原則?沒錯,違反了以下兩個原則:
第二設(shè)計原則
針對于接口編程犁嗅,不針對實現(xiàn)編程
第三設(shè)計原則
多用組合边涕,少用繼承
那么我們應(yīng)該怎么修改這個設(shè)計呢?
2.利用繼承對Beverage類進行改造
首先,我們考慮對基類Beverage類進行修改功蜓,我們根據(jù)前面“類爆炸”進行分析园爷。主要飲料包含各種調(diào)料(牛奶,豆?jié){式撼,摩卡童社,奶泡。著隆。叠洗。。)旅东。
所以修改后的Beverage類的結(jié)構(gòu)如下:
Beverage類具體實現(xiàn)如下:
public class Beverage {
protected String description;//飲料簡介
protected boolean milk=false;//是否有牛奶
protected boolean soy=false;//是否有豆?jié){
protected boolean cocha=false;//是否有摩卡
protected boolean whip=false;//是否有奶泡
protected double milkCost=1.01;//牛奶價格
protected double soyCost=1.03;//豆?jié){價格
protected double cochaCost=2.23;//摩卡價格
protected double whipCost=0.89;//奶泡價格
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean hasMilk() {
return milk;
}
public void setMilk(boolean milk) {
this.milk = milk;
}
public boolean hasSoy() {
return soy;
}
public void setSoy(boolean soy) {
this.soy = soy;
}
public boolean hasCocha() {
return cocha;
}
public void setCocha(boolean cocha) {
this.cocha = cocha;
}
public boolean hasWhip() {
return whip;
}
public void setWhip(boolean whip) {
this.whip = whip;
}
public double getCochaCost() {
return cochaCost;
}
public void setCochaCost(double cochaCost) {
this.cochaCost = cochaCost;
}
public double getWhipCost() {
return whipCost;
}
public void setWhipCost(double whipCost) {
this.whipCost = whipCost;
}
public double cost(){
double condiments=0.0;
if(hasMilk()){//是否需要牛奶
condiments+=milkCost;
}
if(hasSoy()){//是否需要豆?jié){
condiments+=soyCost;
}
if(hasCocha()){//是否需要摩卡
condiments+=cochaCost;
}
if(hasWhip()){//是否需要奶泡
condiments+=whipCost;
}
return condiments;
}
}
實現(xiàn)其中一個子類DarkRoast:
public class DarkRoast extends Beverage{
public DarkRoast(){
description="Most Excellent Dark Roast!";
}
public double cost(){
return 1.99+super.cost();
}
}
看起來很完美,也能滿足現(xiàn)有的業(yè)務(wù)需求十艾,但是仔細思考一下抵代,真的這樣設(shè)計不會出錯?
回答肯定是會出錯忘嫉。
- 第一荤牍,一旦調(diào)料的價格發(fā)生變化,會導(dǎo)致我們隊原有代碼進行大改庆冕。
- 第二康吵,一旦出現(xiàn)新的調(diào)料,我們就需要加上新的方法访递,并需要改變超類Beverage類中cost()方法晦嵌。
- 第三,如果星巴茲咖啡研發(fā)新的飲料拷姿。對于這些飲料而言惭载,某些調(diào)料可能并不合適,但是子類仍然會繼承那些本就不合適的方法响巢,例如我就想要一杯水描滔,加奶泡(hasWhip)就不合適。
- 第四踪古,如果用戶需要雙倍的摩卡咖啡含长,又應(yīng)該怎么辦呢?
3.開放-關(guān)閉原則
到這里伏穆,我們可以推出最重要的設(shè)計原則之一:
第五設(shè)計原則:
類應(yīng)該對拓展開放拘泞,對修改關(guān)閉。
那么什么是開放蜈出,什么又是關(guān)閉田弥?開放就是允許你使用任何行為來拓展類,如果需求更改(這是無法避免的)铡原,就可以進行拓展偷厦!關(guān)閉在于我們花費很多時間完成開發(fā)商叹,并且已經(jīng)測試發(fā)布,針對后續(xù)更改只泼,我們必須關(guān)閉原有代碼防止被修改剖笙,避免造成已經(jīng)測試發(fā)布的源碼產(chǎn)生新的bug。
綜合上述說法请唱,我們的目標(biāo)在于允許類拓展弥咪,并且在不修改原有代碼的情況下,就可以搭配新的行為十绑。如果能實現(xiàn)這樣的目標(biāo)聚至,帶來的好處將相當(dāng)可觀。在于代碼會具備彈性來應(yīng)對需求改變本橙,可以接受增加新的功能用來實現(xiàn)改變的需求扳躬。沒錯,這就是拓展開放甚亭,修改關(guān)閉贷币。
那么有沒有可以參照的實例可以分析呢?有亏狰,就在第二篇我們介紹觀察者模式時役纹,我們介紹到可以通過增加新的觀察者用來拓展主題,并且無需向原主題進行修改暇唾。
我們是否需要每個模塊都設(shè)計成開放--關(guān)閉原則促脉?不用,也很難辦到(這樣的家伙可以稱為“不用設(shè)計模式會死病”)策州。因為想要完全符合開放-關(guān)閉原則嘲叔,會引入大量的抽象層,增加原有代碼的復(fù)雜度抽活。我們應(yīng)該區(qū)分設(shè)計中可能改變的部分和不改變的部分(第一設(shè)計原則)硫戈,針對改變部分使用開放--關(guān)閉原則。
4.裝飾模式
這里下硕,就到了開放--關(guān)閉原則的運用模式-----裝飾者模式丁逝。首先我們還是從星巴茲咖啡的案例來做一個簡單的分析。
分析之前兩個版本(類爆炸 和 繼承大法)的實現(xiàn)方式梭姓,并不能適用于所有的子類霜幼。
這就需要一個新的設(shè)計思路。這里誉尖,我們將以飲料為主罪既,然后運行的時候以飲料來“裝飾”飲料。舉個栗子,如果用戶需要摩卡和奶泡深焙咖啡琢感,那么要做的是:
拿一個深焙咖啡(DarkRosat)對象
以摩卡(Mocha)對象裝飾它
以奶泡(Whip)對象裝飾它
調(diào)用cost()方法丢间,并依賴委托(delegate)將調(diào)料的價錢加上去。
具體的實現(xiàn)我們用一張圖來展示
-
首先我們構(gòu)建DarkRoast對象
-
假如顧客需要摩卡(Mocha)驹针,再建立一個Mocha對象烘挫,并用DarkRoast對象包起來。
-
如果顧客也想要奶泡(Whip),就建立一個Whip裝飾者柬甥,并將它用Mocha對象包起來饮六。
最后運算客戶的賬單的時候,通過最外層的裝飾者Whip的cost()就可以辦得到苛蒲。Whip的cost()會委托他的裝飾對象(Mocha)計算出價格卤橄,再加上奶泡(Whip)的價格。
通過對星巴茲咖啡的設(shè)計方案分析臂外,我們可以發(fā)現(xiàn)虽风,所有的裝飾類都具備以下幾個特點:
裝飾者和被裝飾對象有相同的超類型。
你可以用一個或多個裝飾者包裝一個對象寄月。
既然裝飾者和被裝飾對象有相同的超類型,所以在任何需要原始對象(被包裝的)的場合无牵,可以用裝飾過的對象代替它漾肮。
裝飾者可以在所委托被裝飾者的行為之前與/或之后,加上自己的行為茎毁,以達到特定的目的克懊。
對象可以在任何時候被裝飾,所以可以在運行時動態(tài)地七蜘、不限量地用你喜歡的裝飾者來裝飾對象
什么是裝飾模式呢谭溉?我們首先來看看裝飾模式的定義:
裝飾者模式動態(tài)地將責(zé)任附加到對象上。
若要擴展功能橡卤,裝飾者提供了比繼承更有彈性
的替代方案扮念。
定義雖然已經(jīng)定義了裝飾者模式的“角色”,但是未說明怎么在我們的實現(xiàn)中如何使用它們碧库。我們繼續(xù)在星巴茲咖啡中來熟悉相關(guān)的操作柜与。
其中裝飾者層級可以無限發(fā)展下去,不是如圖中一般兩層關(guān)系嵌灰。并且組件也并非只有一個弄匕,可以存在多個。
現(xiàn)在我們就在星巴茲咖啡里運用裝飾者模式:
到這里沽瞭,我們隊裝飾者模式已經(jīng)有了一個基本的認識迁匠。那么我們已經(jīng)解決了上面提到的四個問題:
- 第一,一旦調(diào)料的價格發(fā)生變化,會導(dǎo)致我們隊原有代碼進行大改城丧。
- 第二延曙,一旦出現(xiàn)新的調(diào)料,我們就需要加上新的方法芙贫,并需要改變超類Beverage類中cost()方法搂鲫。
- 第三,如果星巴茲咖啡研發(fā)新的飲料磺平。對于這些飲料而言魂仍,某些調(diào)料可能并不合適,但是子類仍然會繼承那些本就不合適的方法拣挪,例如我就想要一杯水擦酌,加奶泡(hasWhip)就不合適。
- 第四菠劝,如果用戶需要雙倍的摩卡咖啡赊舶,又應(yīng)該怎么辦呢?
那么根據(jù)第四個問題赶诊,假如我們需要雙倍摩卡豆?jié){奶泡拿鐵咖啡時笼平,該如何去運算賬單呢?首先舔痪,我們先把前面的深度烘焙摩卡咖啡的設(shè)計圖放在這里寓调。
然后我們只需要將Mocha的裝飾者加一,即可
實現(xiàn)星巴茲咖啡代碼
前面已經(jīng)把設(shè)計思想都設(shè)計出來了锄码,接下來是將其具體實現(xiàn)了夺英。首先從Beverage類下手
public abstract class Beverage1 {
String description="Unknown Beverage";
public String getDescription(){
return description;
}
public abstract double cost();
}
Beverage類非常簡單,然后再實現(xiàn)Condiment(調(diào)料類)滋捶,該類為抽象類痛悯,也為裝飾者類
public abstract class CondimentDecorator extends Beverage1{
//所有的調(diào)料裝飾者都必須重新實現(xiàn) getDescription()方法。
public abstract String getDescription();
}
前面已經(jīng)有了飲料的基類重窟,那么我們來實現(xiàn)一些具體的飲料類载萌。首先從濃縮咖啡(Espresso))開始,這里需要重寫cost()方法和getDescription()方法
public class Espresso extends Beverage1{
public Espresso(){
//為了要設(shè)置飲料的描述巡扇,我 們寫了一個構(gòu)造器炒考。記住, description實例變量繼承自Beverage1
description="Espresso";
}
public double cost() {
//最后霎迫,需要計算Espresso的價錢斋枢,現(xiàn)在不需要管調(diào)料的價錢,直接把Espresso的價格$1.99返回即可知给。
return 1.99;
}
}
再實現(xiàn)一個類似的飲料HouseBlend類瓤帚。
public class HouseBlend extends Beverage1{
public HouseBlend(){
description="HouseBlend";
}
public double cost() {
return 0.89;
}
}
重新設(shè)計DarkRoast1
public class DarkRoast1 extends Beverage1{
public DarkRoast1(){
description="DarkRoast1";
}
public double cost() {
return 0.99;
}
}
接下來就是調(diào)料的代碼描姚,我們一開始已經(jīng)實現(xiàn)了抽象組件類(Beverage),有了具體的組件(HouseBlend)戈次,也有了已經(jīng)完成抽象裝飾者(CondimentDecorator)⌒保現(xiàn)在只需要實現(xiàn)具體的裝飾者。首先我們先完成摩卡(Mocha)
public class Mocha extends CondimentDecorator{
/**
* 要讓Mocha能夠引用一個Beverage怯邪,采用以下做法
* 1.用一個實例記錄飲料绊寻,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在實例變量中。這里的做法是:
* 把飲料當(dāng)作構(gòu)造器的參數(shù)悬秉,再由構(gòu)造器將此飲料記錄在實例變量中
*/
Beverage1 beverage;
public Mocha(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這里將調(diào)料也體現(xiàn)在相關(guān)參數(shù)中
return beverage.getDescription()+",Mocha";
}
/**
* 想要計算帶摩卡的飲料的價格澄步,需要調(diào)用委托給被裝飾者,以計算價格和泌,
* 然后加上Mocha的價格村缸,得到最終的結(jié)果。
*/
public double cost() {
return 0.21+beverage.cost();
}
}
還有奶泡(Whip)類
public class Whip extends CondimentDecorator{
/**
* 要讓W(xué)hip能夠引用一個Beverage武氓,采用以下做法
* 1.用一個實例記錄飲料梯皿,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在實例變量中。這里的做法是:
* 把飲料當(dāng)作構(gòu)造器的參數(shù)县恕,再由構(gòu)造器將此飲料記錄在實例變量中
*/
Beverage1 beverage;
public Whip(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這里將調(diào)料也體現(xiàn)在相關(guān)參數(shù)中
return beverage.getDescription()+",Whip";
}
/**
* 想要計算帶奶泡的飲料的價格东羹,需要調(diào)用委托給被裝飾者,以計算價格忠烛,
* 然后加上Whip的價格属提,得到最終的結(jié)果。
*/
public double cost() {
return 0.22+beverage.cost();
}
}
豆?jié){Soy類
public class Soy extends CondimentDecorator{
/**
* 要讓Soy能夠引用一個Beverage况木,采用以下做法
* 1.用一個實例記錄飲料,也就是被裝飾者
* 2.想辦法讓被裝飾者(飲料)被記錄在實例變量中旬迹。這里的做法是:
* 把飲料當(dāng)作構(gòu)造器的參數(shù)火惊,再由構(gòu)造器將此飲料記錄在實例變量中
*/
Beverage1 beverage;
public Soy(Beverage1 beverage) {
this.beverage=beverage;
}
public String getDescription() {
//這里將調(diào)料也體現(xiàn)在相關(guān)參數(shù)中
return beverage.getDescription()+",Soy";
}
/**
* 想要計算帶豆?jié){的飲料的價格,需要調(diào)用委托給被裝飾者奔垦,以計算價格屹耐,
* 然后加上Soy的價格,得到最終的結(jié)果椿猎。
*/
public double cost() {
return 0.21+beverage.cost();
}
}
接下來就是調(diào)用測試類惶岭,具體實現(xiàn)如下:
public class StarbuzzCoffe {
public static void main(String[] args) {
//訂購一杯Espresso,不需要調(diào)料犯眠,打印他的價格和描述
Beverage1 beverage=new Espresso();
System.out.println(beverage.getDescription()+"$"
+beverage.cost());
//開始裝飾雙倍摩卡+奶泡咖啡
Beverage1 beverage2=new DarkRoast1();
beverage2=new Mocha(beverage2);
beverage2=new Mocha(beverage2);
beverage2=new Whip(beverage2);
System.out.println(beverage2.getDescription()+"$"
+beverage2.cost());
//
Beverage1 beverage3=new HouseBlend();
beverage3=new Soy(beverage3);
beverage3=new Mocha(beverage3);
beverage3=new Whip(beverage3);
System.out.println(beverage3.getDescription()+"$"
+beverage3.cost());
}
}
運行結(jié)果:
到這里雌团,我們已經(jīng)完成裝飾者模式對于星巴茲咖啡的改造篇裁。
5.Java中的真實裝飾者
前面已經(jīng)研究了裝飾者模式的原理和實現(xiàn)方式,那么在JAVA語言本身是否有裝飾者模式的使用范例呢,答案是肯定有的,那就是I/O流挡鞍。
第一次查閱I/O源碼,都會覺得類真多,而且一環(huán)嵌一環(huán),閱讀起來會非常麻煩艇挨。但是只要清楚I/O是根據(jù)裝飾者模式設(shè)計,就很容易理解韭赘。我們先來看一下一個范例:
分析一下,其中BufferedInputStream及LineNumberInputStream都擴展自
FilterInputStream缩滨,而FilterInputStream是一個抽象的裝飾類。這樣看有些抽象泉瞻,我們將其中的類按照裝飾者模式進行結(jié)構(gòu)化脉漏,方便理解。
我們發(fā)現(xiàn)瓦灶,和星巴茲的設(shè)計相比鸠删,java.io其實并沒有多大的差異。但是從java.io流我們也會發(fā)現(xiàn)裝飾者模式一個非常嚴(yán)重的"缺點":使用裝飾者模式贼陶,常常會造成設(shè)計中有大量的小類刃泡,數(shù)量還非常多,這對于學(xué)習(xí)API的程序員來說就增加了學(xué)習(xí)難度和學(xué)習(xí)成本碉怔。但是烘贴,懂得裝飾者模式以后會非常容易理解和設(shè)計相關(guān)的類。
6.設(shè)計自己的IO類
在理解裝飾者模式和java.io的設(shè)計后撮胧,我們將磨煉下自己的熟悉程度桨踪,沒錯,就是自己設(shè)計一個Java I/O裝飾者芹啥,需求如下:
編寫一個裝飾者锻离,把輸入流內(nèi)的所有大寫字符轉(zhuǎn)成小寫。舉例:當(dāng)讀
取“ ASDFGHJKLQWERTYUIOPZXCVBNM”墓怀,裝飾者會將它轉(zhuǎn)成“ asdghjklqwertyuiopzxcvbnm”汽纠。具體的辦法在于擴展FilterInputStream類,并覆蓋read()方法就行了傀履。
public class LowerCaseInputStream extends FilterInputStream{
public LowerCaseInputStream(InputStream inputStream){
super(inputStream);
}
public int read() throws IOException {
int c=super.read();
//判斷相關(guān)的字符是否為大寫虱朵,并轉(zhuǎn)為小寫
return (c==-1?c:Character.toLowerCase((char)c));
}
/**
*
*針對字符數(shù)組進行大寫轉(zhuǎn)小寫操作
* @see java.io.FilterInputStream#read(byte[], int, int)
*/
public int read(byte[] b, int off, int len) throws IOException {
int result=super.read(b,off,len);
for(int i=off;i<off+result;i++){
b[i]=(byte) Character.toLowerCase((char)b[i]);
}
return result;
}
}
接下來我們構(gòu)建測試類InputTest
public class InputTest {
public static void main(String[] args) {
int c;
try {
InputStream inputStream=new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
while((c=inputStream.read())>=0){
System.out.print((char)c);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
其中test.txt的內(nèi)容可以自行編輯,放在項目根目錄下我的內(nèi)容原文為:
運行結(jié)果為:
7.總結(jié)
至此钓账,我們已經(jīng)掌握了裝飾者模式的相關(guān)知識點碴犬。總結(jié)一下:
第五設(shè)計原則
類應(yīng)該對拓展開放梆暮,對修改關(guān)閉服协。
裝飾者模式動態(tài)地將責(zé)任附加到對象上。
若要擴展功能啦粹,裝飾者提供了比繼承更有彈性
的替代方案蚯涮。
相應(yīng)的資料和代碼托管地址https://github.com/jixiang52002/HFDesignPattern