no_mkd
一。什麼是裝飾者模式?
裝飾者模式能夠完美實現「對修改關閉,對擴展開放」的原則,也就是說我們可以在不修改被裝飾者的前提下,擴展被裝飾者的功能。再來看看我們的文件操作代碼:
package DecoratorPattern;/**
- @author ayqy
- 定義 Beverage 超類,所有具體 Beverage 和 Ingredient 都必須擴展自此類
*/ public abstract class Beverage { String desc = "Unknown Beverage";//定義飲料相關描述信息 float cost;//定義飲料的價格
public abstract float getCost();//定義 cost 方法返回該飲料的價格,子類必須實現此方法
public String getDesc(){
return desc;
}
}
被「包裹」在最內層的 InputStream 對象是 new FileInputStream(file),是基本的文件輸入流,用 BufferedInputStream 對象來擴展它的功能,甚至我們還可以這樣:
P.S.注意上面用到的動詞——「包裹」,這就是裝飾者模式的核心了
InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));
繼續添加一層新的裝飾 LineNumberInputStream,以擴展獲取行號的功能,當然,我們還可以擴展更多的功能,只要繼續添加新的裝飾就好了。回過頭來想想,我們給 FileInputStream 添加了一層層裝飾,獲得了一個個功能,在此過程中,我們實現了功能的動態擴展,但並沒有修改被裝飾者 FileInputStream 的任何東西。這就是所謂的「對修改關閉,對擴展開放」原則。
二。舉個例子
假設我們要開店賣 Milk,可選的配料有摩卡 Mocha(巧克力味),咖啡 Coffee,冰水 IceWater,當然,如果賣得好的話我們還打算引進新的飲料(Orange、Yoghurt 等等)以及新的配料(Salt。。玩笑)Milk 本身有價格,並且會在節假日打折,各種不同的配料價格也不同,當然,我也可以點一杯加雙份 IceWater 的 Milk。。最容易想到的解決方案是:
定義一個 Milk 類,包含很多屬性,例如 hasMocha, hasCoffee, hasIceWater(用來表示已添加的配料),還需要 discount 屬性(用來表示折扣信息)、cost 屬性(用來表示價格)。這就就好了嗎?不,除此之外我們還需要 MochaNum, CoffeeNum, IceWaterNum(用來表示配料的份數,重口味顧客需要雙份或者更多的配料。。)
問題解決了,可是這樣做真的好嗎?
考慮一下這些情況:1.引進一種新的飲料 Orange(我們需要定義一個 Orange,幾乎沒有複用的部分,從零開始。。或者,我們可以定義一個 Beverage 基類,把飲料共有的部分放進去);2.引進一種新的配料 Salt(我們必須修改 Milk 類,添加 hasSalt, SaltNum 屬性以滿足加鹽 Milk 需求。。)
現在看來我們的解決方案很差,不能適應任何變化,要擴展功能就可能必須修改已有的封裝好的代碼,而且還存在一個性能上的問題:計算飲料價格部分需要大量的 if...else...結構,使得我們的代碼很臃腫,且難以複用(不同飲料配料可能不同,計算價格的方法也不同)。那麼,是時候嘗試裝飾者模式了
首先,因為被裝飾者與裝飾者必須要具有相同的超類(暫不解釋為什麼),所以,我們定義下面的 Beverage 基類:
package DecoratorPattern;/**
- @author ayqy
- 定義 Beverage 超類,所有具體 Beverage 和 Ingredient 都必須擴展自此類
*/ public abstract class Beverage { String desc = "Unknown Beverage";//定義飲料相關描述信息 float cost;//定義飲料的價格
public abstract float getCost();//定義 cost 方法返回該飲料的價格,子類必須實現此方法
public String getDesc(){
return desc;
}
}
有了 Beverage 就可以開始定義被裝飾者——Milk:
package DecoratorPattern;/**
- @author ayqy
- 定義具體 Beverage:Milk 類
*/ public class Milk extends Beverage{
float discount = 1;//定義折扣,節假日 Milk 可能會打折(默認不打折)
public float getDiscount() {
return discount;
}
public void setDiscount(float discount) {
this.discount = discount;
}
public Milk(){
cost = 4.5f;//初始化 Milk 的價格
desc = "Milk";//初始化描述信息
}
@Override
public float getCost(){
return discount * cost;//返回打折後的價格
}
}
接下來是裝飾者,因為裝飾者具有一些不同於 Beverage 的特性,所以我們對其進行抽象:
package DecoratorPattern;/**
- @author ayqy
- 定義 Ingredient 佐料類,繼承自 Beverage(在裝飾者模式中,裝飾者與被裝飾者必須具有相同的超類)
*/ public abstract class Ingredient extends Beverage{
Beverage beverage;//需要添加該佐料的飲料
@Override
public String getDesc() {
return "(" + desc + ")" + beverage.getDesc();//佐料的描述應當帶上括號,以區別佐料與飲料
}
@Override
public float getCost() {
return cost + beverage.getCost();//配料沒有折扣,直接返回其價格 + 飲料價格
}
//在此添加其它 Ingredient 不同於 Beverage 的屬性與行為
}
注意上面的 getDesc 與 getCost 方法,我們把計算價格與生成描述信息的責任完全委託給方法調用機制了,以至於代碼是如此的簡潔。。
下面定義具體配料——IceWater,Coffee,Mocha:
package DecoratorPattern;/**
- @author ayqy
- 定義配料 IceWater 冰水
*/ public class IceWater extends Ingredient{
public IceWater(Beverage bev)
{
cost = 0.5f;
desc = "IceWater";
beverage = bev;
}
}
package DecoratorPattern;
/**
* @author ayqy
* 定義配料 Coffee 咖啡
*
*/
public class Coffee extends Ingredient{
public Coffee(Beverage bev)
{
cost = 3;
desc = "Coffee";
beverage = bev;
}
}
package DecoratorPattern;
/**
* @author ayqy
* 定義配料 Mocha 摩卡
*
*/
public class Mocha extends Ingredient{
public Mocha(Beverage bev)
{
cost = 2;
desc = "Mocha";
beverage = bev;
}
}
一切準備就緒,我們的 Milk 小店可以開張了。。
三。效果示例
先定義一個測試類:
package DecoratorPattern;public class Test { public static void main(String[] args){ Beverage bev;
System.out.println("做一杯加摩卡加咖啡的 Milk。。"); bev = new Milk();//先做一杯 Milk bev = new Mocha(bev);//添加 Mocha bev = new Coffee(bev);//添加 Coffee System.out.println(bev.getDesc() + " " + bev.getCost() + "¥"); System.out.println("做一杯加雙份冰水雙份摩卡的咖啡 Milk。。"); bev = new Milk();//重新做一杯 Milk bev = new IceWater(bev);//添加冰水 bev = new IceWater(bev);//添加冰水 bev = new Mocha(bev);//添加 Mocha bev = new Mocha(bev);//添加 Mocha bev = new Coffee(bev);//添加 Coffee System.out.println(bev.getDesc() + " " + bev.getCost() + "¥"); //當然也可以這樣寫:Beverage bev = new Coffee(new Mocha(new Milk())); //對比我們熟悉的方法鏈:InputStream in = new BufferedInputStream(new FileInputStream(file)); }}
結果示例:

效果不錯吧,再考慮之前的擴展問題:
1.引進一種新的飲料 Orange(我們需要定義一個 Orange 類繼承自 Beverage 基類,可以複用 Beverage 基類中已有的部分,如果還不滿意,當然也可以抽象出一個「具體飲料類 ConcreteBeverage」,讓 Milk 等其它飲料在此基礎上擴展);2.引進一種新的配料 Salt(我們不必對 Milk 類做任何修改,只需要實現一種 Salt 配料,繼承自 Ingredient 類就好了)
發現裝飾者模式的優点了嗎?那麼是時候潑一盆冷水了。。
四。裝飾者模式的優缺點
缺點其實顯而易見——你見過這麼長的代碼嗎?
XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));
嗯,它只是給被裝飾對象做了三次功能擴展而已,當然,還可以更多。。也就意味著可以更長。而且,我們在使用時創建了很多小對象,就像這樣:
bev = new Milk();//重新做一杯 Milk bev = new IceWater(bev);//添加冰水 bev = new IceWater(bev);//添加冰水 bev = new Mocha(bev);//添加 Mocha bev = new Mocha(bev);//添加 Mocha bev = new Coffee(bev);//添加 Coffee
讓一個不熟悉裝飾者模式的人來讀上面的代碼,他能很快弄明白嗎?
注意,上面的代碼就解釋了開篇提到的動詞——「包裹」,對嗎?
優點:
除了上面提到的動態擴展優點,還有一個更重要的優點就是前面提到的 getDesc 與 getCost 方法
沒錯,我們可以利用這種調用機制來完成我們的操作(在裝飾動作前或者裝飾動作後添加我們的自定義操作就好了,例子裏其實屬於在裝飾動作後添加操作),我們很輕易的達到了類似於遞歸的效果
這也就解釋了「為什麼裝飾者與被裝飾者要具有相同的超類?」,還需要更多一點的解釋:
有一種設計原則是「多用組合,少用繼承」,這裡我們好像違背了這個原則吧
其實並沒有違背原則,裝飾者模式中的繼承是為了獲得類型的匹配,而不是為了利用繼承來擴展類的行為,而「多用組合,少用繼承」原則省略掉的前提條件是「(當我們需要擴展類的行為時)多用組合,少用繼承」
暫無評論,快來發表你的看法吧