跳到主要內容
黯羽輕揚每天積累一點點

設計模式之裝飾者模式(Decorator Pattern)

免費2015-03-06#Design_Pattern#装饰者模式#Decorator Pattern

裝飾者模式是一種在框架中應用廣泛的設計模式,在 JavaAPI 中文件流操作就應用了這樣的模式,例如 InputStream in = new BufferedInputStream(new FileInputStream(file));其中,BufferedInputStream 被用來裝飾 FileInputStream,從而實現功能的擴展。裝飾者模式滿足「對修改關閉,對擴展開放」的 OO 設計原則,是一種很不錯的設計模式。

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 方法

沒錯,我們可以利用這種調用機制來完成我們的操作(在裝飾動作前或者裝飾動作後添加我們的自定義操作就好了,例子裏其實屬於在裝飾動作後添加操作),我們很輕易的達到了類似於遞歸的效果

這也就解釋了「為什麼裝飾者與被裝飾者要具有相同的超類?」,還需要更多一點的解釋:

有一種設計原則是「多用組合,少用繼承」,這裡我們好像違背了這個原則吧

其實並沒有違背原則,裝飾者模式中的繼承是為了獲得類型的匹配,而不是為了利用繼承來擴展類的行為,而「多用組合,少用繼承」原則省略掉的前提條件是「(當我們需要擴展類的行為時)多用組合,少用繼承」

評論

暫無評論,快來發表你的看法吧

提交評論