設計模式(四)一文搞明白裝飾者模式

關於裝飾者模式的定義,我就直接引用Head First了:裝飾者模式動態的將責任附加到對象上。若要擴展功能,裝飾者提供了比繼承更有彈性的替代方案 。 其實裝飾者模式的重點在於給對象動態的附加職責,通過對象組合的方式,運行時裝飾對象,在不改變任何底層代碼的情況下,給現有對象賦予新的職責。

裝飾者模式

現在我們要爲賣煎餅的大媽設計一套系統,讓大媽能更好的算賬收錢。大媽主要經營煎餅(7元),並可以往其中添加配料烤腸(1元),雞蛋(1元),如果生意紅火,可能會考慮擴大經營其他小吃或添加配料種類。現需要設計出一套系統,以便快速計算出每位顧客所購小吃的價格。

這是一個食品類接口。同樣,要符合針對接口編程,不針對實現編程的OO原則。

public interface ISnack {
	// 食物的描述
	String getDescription();
	// 食物的價格
	double cost();
}

這是一個配料類接口。配料類接口IDecoratorSnack繼承自ISnack接口,因爲需要兩接口的實現類(裝飾者與被裝飾對象)具有相同的超類型,只有這樣IDecoratorSnack接口才能取代ISnack接口。

/**
 * 這是配料類的接口。
 * 
 * <p>裝飾者與被裝飾者需要具有相同的接口,
 * 這樣用戶使用裝飾者對象纔會和使用原始對象一樣。
 */
public interface IDecoratorSnack extends ISnack {
	// 可根據需要擴展屬性,如配料大份,小份等
}

這是具體的食品-煎餅類

public class PancakeSnack implements ISnack {
	@Override
	public String getDescription() {
		return "煎餅";
	}
	
	@Override
	public double cost() {
		return Price.PancakeSnack.price;
	}
}

食品類(被裝飾者)既可以單獨使用,也可以被配料類(裝飾者)包着使用,因爲裝飾者和被裝飾者對象具有相同的超類型,所以在任何需要原始對象(被包裝的)的場合,都可以用裝飾過的對象替代它

如果,我想要一份加雞蛋和烤腸的煎餅價格是多少?單價類見下:

public enum Price {
	//雞蛋、烤腸、煎餅
	Egg(1.0), Sausage(1.0), PancakeSnack(7.0);
	
	double price;

	private Price(double price) {
		this.price = price;
	}
}

這是我們的具體配料雞蛋類

public class Egg implements IDecoratorSnack {
	// 持有被修飾的對象
	ISnack iSnack;
	
	public Egg(ISnack iSnack) {
		this.iSnack = iSnack;
	}
	
	@Override
	public String getDescription() {
		return iSnack.getDescription() + ",加雞蛋";
	}
	
	@Override
	public double cost() {
	    // 被修飾對象的價格 + 雞蛋價格
		return iSnack.cost() + Price.Egg.price;
	}
}

這是具體的烤腸配料類

public class Sausage implements IDecoratorSnack{
	//持有被修飾對象的引用
	ISnack iSnack;
	
	public Sausage(ISnack iSnack) {
		this.iSnack = iSnack;
	}
	
	@Override
	public String getDescription() {
		return iSnack.getDescription() + ",加香腸";
	}
	
	@Override
	public double cost() {
		// 被修飾對象的價格 + 烤腸價格
		return iSnack.cost() + Price.Sausage.price;
	}
}

每個裝飾者都持有一個被裝飾者的對象,這個被裝飾者對象不一定是原始對象,也可能是被包裝了多層的對象。通過這種組合,加入了新的行爲。符合對擴展開放,對修改關閉的OO原則

我們來收錢吧。我們能夠發現,我們能夠使用被裝飾的對象就像使用原始對象一樣,這歸功於裝飾者與被裝飾者具備同樣的接口。同樣一個原始對象是能夠被包裝多層的,而在使用者的眼裏,它只是一個被賦予了新功能的原始對象而已。

public class DecoratorPatternTest {
	
	public static void main(String[] ags) {
        // 煎餅 + 雞蛋 + 烤腸
		ISnack snackOneISnack = new PancakeSnack();
		snackOneISnack = new Egg(snackOneISnack);
		snackOneISnack = new Sausage(snackOneISnack);
		// 這樣就能夠打印出,我們的(煎餅 + 雞蛋 + 烤腸)價格和描述了
		System.out.println(snackOneISnack.getDescription() + "價格:" + snackOneISnack.cost() + "元");
	}
}

而且擴展起來方便,我們隨時加入不同種類小吃和配料類。如我們現在要加一種烤冷麪類

public class ColdRoastSnake implements ISnack{
	@Override
	public String getDescription() {
		return "烤冷麪";
	}
	
	@Override
	public double cost() {
		return Price.ColdRoastSnake.price;
	}
}

我們再添加一個雞裏脊肉類

public class Chicken implements IDecoratorSnack{	
	ISnack iSnack;
	
	public Chicken(ISnack iSnack) {
		this.iSnack = iSnack;
	}
	
	@Override
	public String getDescription() {
		return iSnack.getDescription() + ",加裏脊肉";
	}
	
	@Override
	public double cost() {
		return iSnack.cost() + Price.Chicken.price;
	}
}

好了,如果用戶購買烤冷麪+雞蛋+裏脊肉,價格該多少呢?

public class DecoratorPatternTest {
	
	public static void main(String[] ags) {
        // 烤冷麪 + 雞蛋 + 裏脊肉
		ISnack snackOneISnack = new ColdRoastSnake();
		snackOneISnack = new Egg(snackOneISnack);
		snackOneISnack = new Chicken(snackOneISnack);
		// 這樣就能夠打印出,我們的(烤冷麪 + 雞蛋 + 裏脊肉)價格和描述了
		System.out.println(snackOneISnack.getDescription() + "價格:" + snackOneISnack.cost() + "元");
	}
}

需要注意的是,通過利用裝飾者模式,會造成設計中產生大量的小類,如果過度使用,會使程序變得很複雜。另外可能還會出現類型問題,如果把代碼寫成依賴於具體的被裝飾者類型,不針對抽象接口進行編程,那麼就會出現問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章