1. 程式人生 > >軟體設計模式之(二)裝飾者模式

軟體設計模式之(二)裝飾者模式

歡迎大家提出意見,一起討論!

 例子程式碼:(編譯工具:Eclipse)

參考書籍: <<軟體祕笈-----設計模式那點事>>

裝飾者模式(Decorator ['dekəreitə]  Pattren),是在不改變原類檔案和使用繼承的情況下,動態地擴充套件一個物件的功能,它是通過建立一個包裝物件,也就是

裝飾來包裹真實的物件

使用裝飾者模式的時候需要注意以下幾點內容: (1)裝飾物件和真實物件有相同的介面。這樣客戶端物件就可以以和真實物件相同的方式和裝飾物件互動。 (2)裝飾物件包含一個真實物件的引用。 (3)裝飾物件接受所有來自客戶端的請求,並把這些請求轉發給真實的物件。
(4)裝飾物件可以在轉發這些請求以前或以後增加一些附加功能。這樣就確保了在執行時,不用修改給定的物件的結構就可以在外部增加附加功能。 在面向物件的設計中,通常是通過繼承來實現對給定的類的功能擴充套件,然而,裝飾者模式不需要子類,可以在應用程式執行時動態擴充套件功能

1、一般化分析

分析一下染色饅頭的製作過程: (1)需要生產一個正常饅頭; (2)為了節省成本(不使用玉米麵),使用染色劑加入到正常饅頭中; (3)通過和麵機攪拌,最後生產出“玉米饅頭”。
/**
 * 饅頭加工介面
 * 
 * @author
 * 
 */
public interface IBread {
	// 準備材料
	public void prepair();

	// 和麵
	public void kneadFlour();

	// 蒸饅頭
	public void steamed();

	/**
	 * 加工饅頭方法
	 */
	public void process();
}

/**
 * 正常饅頭的實現
 * 
 * @author
 * 
 */
public class NormalBread implements IBread {
	// 準備材料
	public void prepair() {
		System.out.println("準備麵粉、水以及發酵粉...");
	}

	// 和麵
	public void kneadFlour() {
		System.out.println("和麵...");
	}

	// 蒸饅頭
	public void steamed() {
		System.out.println("蒸饅頭...香噴噴的饅頭出爐了!");
	}

	/**
	 * 加工饅頭方法
	 */
	public void process() {
		// 準備材料
		prepair();
		// 和麵
		kneadFlour();
		// 蒸饅頭
		steamed();
	}

}

/**
 * 染色的玉米饅頭
 * 
 * @author
 * 
 */
public class CornBread extends NormalBread {
	// 黑心商販 開始染色了
	public void paint() {
		System.out.println("新增檸檬黃的著色劑...");
	}

	// 過載父類的和麵方法
	@Override
	public void kneadFlour() {
		// 在麵粉中加入 染色劑 之後才開始和麵
		this.paint();
		// 和麵
		super.kneadFlour();
	}
}

/**
 * 甜蜜素饅頭
 * 
 * @author
 * 
 */
public class SweetBread extends NormalBread {
	// 黑心商販 開始新增甜蜜素
	public void paint() {
		System.out.println("新增甜蜜素...");
	}

	// 過載父類的和麵方法
	@Override
	public void kneadFlour() {
		// 在麵粉中加入 甜蜜素 之後才開始和麵
		this.paint();
		// 和麵
		super.kneadFlour();
	}
}

我們知道,饅頭的種類是很多的,現在黑心商販又想生產“甜玉米饅頭"了,怎麼辦叱?你可能要說“使用繼承”不就行了嗎? 再建立一個饅頭,繼承正常饅頭類。不可否認,這樣做是不錯的,可以實現要求。然而,反映到類圖上是又多了一個子類,如果還有其他的饅頭種類, 都要繼承嗎?那樣的話類就要爆炸了,龐大的繼承類關係圖。 繼承方式存在這樣兩點不利因素: (1)父類的依賴程式過高,父類修改會影響到子類的行為。 (2)不能複用已有的類,造成子類過多。

2、下面我們使用裝飾者模式重新實現染色饅頭的例項!

使用裝飾者的靜態類圖,結構如圖所示。 (1)為了裝飾正常饅頭NormalBread,我們需要一個正常饅頭一樣的抽象裝飾者:AbstractBread, 該類和正常饅頭類NormalBread一樣實現IBread饅頭介面,不同的是該抽象類含有一個IBread介面型別的私有屬性bread, 然後通過構造方法,將外部IBread介面型別物件傳入。
/**
 * 抽象裝飾者
 * 
 * @author
 * 
 */
public abstract class AbstractBread implements IBread {
	// 儲存傳入的IBread物件
	private final IBread bread;

	public AbstractBread(IBread bread) {
		this.bread = bread;
	}

	// 準備材料
	public void prepair() {
		this.bread.prepair();
	}

	// 和麵
	public void kneadFlour() {
		this.bread.kneadFlour();
	}

	// 蒸饅頭
	public void steamed() {
		this.bread.steamed();
	}

	// 加工饅頭方法
	public void process() {
		prepair();
		kneadFlour();
		steamed();

	}
}
AbstractBread類滿足了裝飾者的要求:和真實物件具有相同的介面;包含一個真實物件的引用;接受所有來自客戶端的請求,並反這些請求轉發給真實的物件; 可以增加一些附加功能。 (2)建立裝飾者。 A、建立染色劑裝飾者----CornDecorator. 建立染色裝飾者"CornDecorator",繼承AbstractBread, 含有修改行為:新增檸檬黃的著色劑。
/**
 * 染色的玉米饅頭
 * 
 * @author
 * 
 */
public class CornDecorator extends AbstractBread {

	// 構造方法
	public CornDecorator(IBread bread) {
		super(bread);
	}

	// 黑心商販 開始染色了
	public void paint() {
		System.out.println("新增檸檬黃的著色劑...");
	}

	// 過載父類的和麵方法
	@Override
	public void kneadFlour() {
		// 在麵粉中加入 染色劑 之後才開始和麵
		this.paint();
		// 和麵
		super.kneadFlour();
	}
}

這和上面提到的CornBread類的內容是一樣的,只是多了一個構造方法。 B、建立甜蜜互裝飾者-----SweetDecorator
/**
 * 甜蜜素饅頭
 * 
 * @author
 * 
 */
public class SweetDecorator extends AbstractBread {
	// 構造方法
	public SweetDecorator(IBread bread) {
		super(bread);
	}

	// 黑心商販 開始新增甜蜜素
	public void paint() {
		System.out.println("新增甜蜜素...");
	}

	// 過載父類的和麵方法
	@Override
	public void kneadFlour() {
		// 在麵粉中加入 甜蜜素 之後才開始和麵
		this.paint();
		// 和麵
		super.kneadFlour();
	}
}

C、生產甜玉米饅頭。 首先建立一個正常饅頭,然後使用甜蜜素裝飾饅頭,之後再用檸檬黃的著色劑裝飾饅頭,最後加工饅頭。
/**
 * 客戶端應用程式
 * 
 * @author
 * 
 */
public class Client {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// 生產裝飾饅頭
		System.out.println("\n====開始裝飾饅頭!!!");
		// 建立普通的正常饅頭例項
		// 這是我們需要包裝(裝飾)的物件例項
		IBread normalBread = new NormalBread();

		// 下面就開始 對正常饅頭進行裝飾了!!!
		// 使用甜蜜素裝飾饅頭
		normalBread = new SweetDecorator(normalBread);
		// 使用檸檬黃的著色劑裝飾饅頭
		normalBread = new CornDecorator(normalBread);
		// 生產饅頭資訊
		normalBread.process();
		System.out.println("====裝飾饅頭結束!!!");

	}

}
執行結果:
====開始裝飾饅頭!!!
準備麵粉、水以及發酵粉...
新增檸檬黃的著色劑...
新增甜蜜素...
和麵...
蒸饅頭...香噴噴的饅頭出爐了!
====裝飾饅頭結束!!!

可能你會感覺比較奇怪,我們先使用的是甜蜜素,然後使用的是著色劑,應該先列印甜蜜素,然後再列印著色劑才對? 不用奇怪,這也不矛盾,因為裝飾者相當於對原有物件的包裝,這就像一個禮品盒,最裡面是一個最普通的紙盒, 然後用一般的紙包裝起來,最後使用比較漂亮的包裝紙包裝,我們最先看到的是最外漂亮的包裝紙,其次才是裡面的一般包裝紙。

3、設計原則

(1)封裝變化部分

設計模式是封裝變化的最好闡釋,無論哪一種設計模式針對的都是軟體中存在的“變化”部分,然後

用抽象對這些“變化”的部分進行封裝。使用抽象的好處在於為軟體的擴充套件提供了很大的方便性。

在裝飾者模式中合理地利用了類繼承和組合的方式,非常靈活地表達了物件之間的依賴關係。

裝飾者模式應用中“變化”的部分是元件的擴充套件功能,裝飾者和被裝飾者完全隔離開來,這樣我們就可以任意地改變裝飾者和被裝飾者。

(2)"開-閉"原則

我們在需要對元件進行擴充套件、增添新的功能行為時,只需要實現一個特定的裝飾者即可,這完全是增量修改,

對原有軟體功能結構沒有影響,對客戶端APP來說也是完全透明的,不必關心內部實現細節。

(3)面向抽象程式設計,不要面向實現程式設計

在裝飾者模式中,裝飾者角色就是抽象類實現,面向抽象程式設計的好處就在於起到了很好的介面隔離作用。在運用時,

我們具體操作的也是抽象類引用,這些顯示了面向抽象程式設計。

(4)優先使用物件組合,而非類繼承。

裝飾者模式最成功在於合理地使用了物件組合方式,通過組合靈活地擴充套件了元件的功能,所有的擴充套件功能都是通過組合而非繼承獲得的,

這從根本上決定了是高內聚、低耦合的。

4、使用場合

(1)當我們需要為某個現有的物件動態地增加一個新的功能或職責時,可以考慮使用裝飾者模式;

(2)當某個物件的職責經常發生變化或者經常需要動態地增加職責,避免為了適應這樣的變化而增加繼承子類擴充套件的方式,因為這種方式會造成子類膨脹的速度過快,難以控制,此時可以使用裝飾者模式。

客戶端不會覺得物件在裝飾前和裝飾後有什麼不同,裝飾者模式可以在不建立更多子類的情況下,將物件的功能加以擴充套件,裝飾者模式使用原來被裝飾的一個子類例項,

把客戶端的呼叫委派到裝飾者。

下面我們來看一下裝飾者模式的靜態類圖.使我們對裝飾者模式有一個更加清晰的認識

A、被裝飾者抽象Component:是一個介面或抽象類,是定義的核心物件。

     在裝飾者模式中,必然有一個被撮出來最核心、最原始的介面。這個類就是我們需要裝飾類的基類。本例中是IBread介面。

B、被裝飾者具體實現ConcreteComponent:這是Component類的一個實現類,我們要裝飾的就是這個具體的實現類。本例中是NormalBread

C、裝飾者Decorator:一般是一個抽象類。它裡面有一個指向Component變數的引用。

D、裝飾者實現ConcreteDecorator1和ConcreteDecorator2.