1. 程式人生 > >設計模式——03 裝飾者模式

設計模式——03 裝飾者模式

3 Decorator Pattern(裝飾者模式)

3.1設計原則一

類應該對擴充套件開放,對修改關閉

         前言:裝飾者模式主要是為了解決繼承濫用的問題,以下將使用物件組合的方式做到在執行時裝飾類。

1)案例分析一:

         REQ1星巴克咖啡店咖啡種類擴充套件飛快,Vander作為其老闆,準備儘快更新訂單系統來滿足這一發展。原先的設計如下:

分析隨著飲品的發展,每種飲料都可以自由搭配,而且本身飲料也很多,除了咖啡之外,鴛鴦、奶茶、可樂、雪碧、酸奶、豆漿等等,而且調料還可以自由選擇。使用不同的調料需要付不同的價格,例如一小份牛奶加入咖啡中,加收1塊錢等等。如果繼續按照上述的設計繼續,則繼承Beverage抽象類的飲料將非常多,例如coffeewithonemilk,milkTeaWithSteamAndSoy等等,這樣能產生無數的搭配,並且牛奶價格上升之後,每個涉及到牛奶的類的cost函式還需要修改,這簡直就是噩夢。

解決方法1:Vander 就開始設計了

         超類cost()將計算所有調料的價格,子類覆蓋的cost()方法擴充套件超類的功能,把指定的飲料型別的價錢也加上。

public class Beverage {

	private String desc;
	
	private boolean milk;
	
	private boolean soy;
	
	private boolean mocha;
	
	private boolean whip;

	public boolean hasMilk() {
		return this.milk;
	}
	
	public boolean hasSoy() {
		return this.soy;
	}
	
	public boolean hasMocha() {
		return this.mocha;
	}
	
	public boolean hasWhip() {
		return this.whip;
	}
	
	public String getDesc() {
		return desc;
	}

	public void setDesc(String desc) {
		this.desc = desc;
	}
	
	public void setMilk(boolean milk) {
		this.milk = milk;
	}

	public void setSoy(boolean soy) {
		this.soy = soy;
	}

	public void setMocha(boolean mocha) {
		this.mocha = mocha;
	}

	public void setWhip(boolean whip) {
		this.whip = whip;
	}

	public float cost() {
		float flavourCost = 0.0f;
		if(hasMilk()) {
			flavourCost = flavourCost + 1.0f;
		} 
		if(hasSoy()) {
			flavourCost = flavourCost + 2.0f;
		}
		if(hasMocha()) {
			flavourCost = flavourCost + 3.0f;
		}
		if(hasWhip()) {
			flavourCost = flavourCost + 4.0f;
		}
		return flavourCost;
	}
	
}

public class DarkRoast extends Beverage {

	private float cost;
	
	public float getCost() {
		return cost + super.cost();
	}

	public void setCost(float cost) {
		this.cost = cost;
	}
	
}

存在的問題:

         這個方法出現了以下4個問題:

         1、當調料價格改變的時候需要修改Beverage類的程式碼。

         2、一旦有新的調料,需要加上新的方法,並改變超類中的cost()方法。

         3、如果有新的飲料(Tea),對這些飲料而言,某些調料(如soy、stream等)可能並不適合,但是這個設計方式中,Tea子類仍將繼承那些不合適的方法,例如:hasSoy()(加入豆漿)

         4、顧客萬一不只是要一份摩卡,想加入兩份摩卡調料,上述的方法根本無法應對。     

         解決方法3(使用裝飾者模式):

         REQ2:首先有這麼一種需求,顧客買了一杯DarkRoast,想加Mocha,然後再加奶泡Whip,最後要計算這杯DarkRoast的金額。

裝飾者模式可以用下面的圖來說明,該圖上可以看到Whip包裹著Mocha,而Mocha又包裹了DarkRoast,並且這三個類的基類都是Beverage,DarkRoast繼承自Beverage,且有一個用來計算費用的cost()方法,Mocha物件是一個裝飾者,它的型別反映了它所裝飾的物件;Whip也是一個裝飾者,所以它也反映了DarkRoast型別,幷包括一個cost()方法。

最後到結賬的時候,先呼叫最外層的Whip,得到了Whip的價格,然後再呼叫Mocha的cost,此時Whip的價格傳給了Mocha,這樣Mocha再加上自己的價格,現在就得到了Mocha+Whip的價格,然後再呼叫DarkRoast的價格,最後就得到了這杯咖啡的價格。這樣相當於就做到“在執行時決定類的行為”。

        

    這裡要說明的是,Beverage可以用介面也可以用抽象類,若需要加入屬性的話,就使用抽象類,若不需要屬性則可以使用介面,方便日後程式碼可以extends其他的類。

以下是關鍵程式碼:

Beverage beverage = new DarkRoast();
beverage = new Whip(beverage);
beverage = new Mocha(beverage);

public class Mocha implements CondimentDecorator {

	private Beverage beverage;
	
	public float cost() {
		return this.beverage.cost() + 1.0f;
	}
	
	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}

	public String getDesc() {
		return this.beverage.getDesc() + " with " + "Mocha";
	}

}

REQ3:那麼問題來了,現實的Java世界中有哪些用了裝飾者模式呢,你是否看過這樣的一句new LineNumberInputStream(new BufferedInputStream(new FileInputStream)),這句話看似很複雜,其實這就是典型的裝飾者模式,FileInputStream是被裝飾的“元件”,Java I/O程式庫提供了幾個元件,包括了FileInputStream、StringBufferInputStream、ByteArrayInputStream… …等。這些類都提供了最基本的位元組讀取功能。

BfferedInputStream是具體的裝飾者,加入了兩種行為,利用快取輸入來改進效能,用readline()方法(用來一次讀取一行文字輸入資料)來增強介面。

LineNumberInputStream也是一個具體的裝飾者,它加上了計算行數的功能。

         下面進行一個小練習,寫一個IO裝飾者來講輸入流中所有的大寫字母轉成小寫。

 

public class LowcaseInputStream extends FilterInputStream {

	public LowcaseInputStream(InputStream in) {
		super(in);
	}
	
	public int read() throws IOException {
		int c = super.read();
		return ( c == -1? c : Character.toLowerCase((char)c));
	}
	
	public int read(byte[] b, int offset, int len) throws IOException {
		int result = super.read(b, offset, len);
		for(int i = offset; i < offset + result; i++) {
			b[i] = (byte)Character.toLowerCase((char)b[i]);
		}
		return result;
	}

}

public class Main {

	public static void main(String[] args) {
		InputStream inputStream;
		int c;
		try {
			inputStream = new FileInputStream("test.txt");
			inputStream = new BufferedInputStream(inputStream);
			inputStream = new LowcaseInputStream(inputStream);
			while((c = inputStream.read()) >= 0) {
				System.out.print((char)c);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}

    裝飾者模式的缺點:利用裝飾者模式,常常造成設計中有大量的小類,數量實在太多,可能造成使用此API程式設計師的困擾。但是其實瞭解了裝飾者的原理,就可以容易地辨別出他們的裝飾者類是如何組織的,以方便用包裝方式取得想要的功能。

面向物件基礎

         抽象、封裝、多型、繼承

四大原則

設計原則一:封裝變化

設計原則二:針對介面程式設計,不針對實現程式設計。

         設計原則三:多用組合,少用繼承。

         設計原則四:為互動物件之間的鬆耦合設計而努力

設計原則五:對擴充套件開放,對修改關閉

 

模式

裝飾者模式:動態地將責任附加到物件上。想要擴充套件功能,裝飾者提供有別於繼承的另一種選擇。

 

最後獻上此次設計的原始碼,原始碼中包括了一些起初錯誤的實現,以及後期經過思考後正確的實現,在此處出現的所有原始碼均有實現,有需要的小夥伴可以下載來執行一下,首先先自己進行設計,然後再參考,這樣才能加深觀察者模式的理解。