裝飾者模式筆記
我們常常看到早餐店的蹤跡,每個街頭巷角幾乎都有一家早餐店在營業。
早餐店可以說是跟杯裝飲料一樣進入門檻極低的行業之一。
為了跟市面上的傳統早餐店競爭,現在有一家早餐店即將開啟,
適逢創業階段,他們的早餐店只賣了四樣主食(飲料在此先省略以單純的思考問題的架構,在未來可以進一步針對飲料店或是其他類型的商店作構思)
因為他們的材料手藝真的不錯,所以開市即熱賣
他們目前導入了訂單系統來管理他們的早餐的物料與營收等資訊。
他們開創期遵循著”少樣多量”的原則來處理他們的早餐商品,但是他們目前希望可以開始擴展他們的品牌與商品多樣化。
他們原先的銷售系統設計如下:
(註:先別問我這張圖的正當性,至少剛開始只有這四種主食就對了)
而上面這張圖是的結構是這樣:”早餐店的食物(菜單)”是一個抽象類別,店內所提供的食物都必須繼承自此類別。
當中的cost方法是抽象的,所以子類別也必須定義自己的cost實踐,每個子類別食物都要實踐cost()來告知整份早餐的價格。
因為目前已經累積了一定的客源與資金,所以這家早餐店準備更新他們訂單系統,來合乎他們新開發出來的早餐菜單。
滿足與吸引他們的早餐顧客,他們打算以彈性的方式提供客人選擇自己想吃的配料,加到不同種類的早餐食物中。
我們可以想像購買早餐時,可以要求各種加料,例如,漢堡我想要加蛋、生菜、雙倍肉,蛋餅也可以加培根、玉米,甚至是都加!!。
不過這些對於早餐店來說這些都是成本,所以早餐店會根據加料酌收費用。所以銷售系統必須考量到這些加料的部分。
這是可能是我們嘗試設計的第一個版本!!
如下圖
哇咧!!這簡直是類別爆炸!!別笑這個版本架構,其實我們看菜單上,往往也都是這樣設計
例如,漢堡加蛋30元,漢堡不加蛋25元,雞排堡35元,都是重覆地顯示在單子上呢!(所以有早餐的單子就很大張,而且字還很小)
正如同上圖,每一個子類別的食物組合,都會透過cost()的方法將計算出主食加上各種佐料的價格。
不過,身為設計模式學習者,我們必須來思考這樣的結構!
從先前的OO守則,很明顯著,這樣的模式設計會遇到維護上的困難。如果起司的價格上揚怎麼辦?!要加入一個新的生菜配料時,怎麼辦??
造成了這種維護上的困難,違反了我們先前提過的兩個設計守則,而且很嚴重!
1.________________________
2.________________________
有興趣的去找找 http://www.dotblogs.com.tw/pin0513/category/3265.aspx
所以你可能會說:「笨透了,幹嘛設計這麼多類別呀?利用實體變數和繼承,就可以追蹤這些配料了呀!!」
所以我們可能可以這樣對早餐店食物的類別下手!!
這樣我們只需要五個類別!!
我們為每種配料加入布林值,有火腿與加火腿等方法則是取得和設定配料的布林值
而現在早餐店的食物類別中的cost方法,不再是一個抽象方法,在這邊必須去實踐它,必須讓他在這邊要加入佐料的價格。子類別仍將覆寫cost方法,但是會調用超類別的cost來計算出基本食物(漢堡、土司、貝果、蛋餅…etc)加上配料的價格。
整體類別圖就變成了:
而超類別實踐cost()的程式碼你可以這樣寫:(只寫虛擬碼)
1: public double cost(){
2: double cost;
3: if (有火腿()) //回傳是否有火腿的布林值
4: cost+= 火腿價;
5: if (有玉米()) //回傳是否有玉米的布林值
6: cost+= 玉米價;
7: …//以此類推
8: return cost;
9: }
接著在子類別中,就可以取得所有佐料價格,再實踐加上基本食物的價格(漢堡土司..etc)。
如此一來,一共只需要五個類別,這就是我們要的做法嗎???!
如果是這樣就不需要介紹設計模式啦!!
想看看,在什麼情況下,會影響到這個設計呢?
1.配料價格改變會使得我們需要更改程式碼
2.一旦出現新的配料(例如,果醬、奶油!),就要加上新的方法,並改變超類別中cost對於配料價格的計算
3.以後可能會開發出新口味的主食,對於某些主食而言,配料可能不適合!(例如蛋餅就不需要果醬= =)
4.萬一顧客想要雙倍起司、雙倍肉排(像麥當勞那樣),怎麼辦?
5.諸如此類!
說了以上這麼多,本書要傳授新的OO守則了!
但是從先前的OO守則我們可以粹取出一些優良的傳統!
也就是說:
繼承雖然強大,但是並不能帶來最有彈性以及最好維護的設計!
而不利過繼承要達到程式碼再利用,我們曾經提到利用合成以及委派,可以在執行期具有繼承的效果(看前幾篇)
利用繼承可以讓子類別的行為在編譯時決定,而透過合成擴充物件的行為,就可以在執行期動態地進行擴充
深入淺出(詳可見深入淺出原書)大師說,新的OO設計守則就是:
類別應該開放,以便擴充;應該關閉,禁止修改
以上的目標是允許類別容易擴充,在不修改既有程式碼的情況下,就可以搭配新的行為
達到這樣的目標,就讓程式具有彈性可以應付改變,可以接受新的功能以達到改變需求的目的。
上述聽起來很矛盾呢…,況且,越難修改的東西,就越難擴充,不是嗎…
不過看到某些OO的設計技巧(例如前一篇觀察者模式),藉由加入新的觀察者,可以在任何時候擴充主題與觀察者。
之後陸續會看更多的技巧,而稍待所建構起來的例子,將完全遵循開放關閉守則(書中的範例更為貼切,有興趣者請見原書)。
雖然這個開放關閉守則有諸多的好處,不過到處的採用這個守則,是一種浪費也不必要,而且會導致程式碼變的複雜而難以理解
更重要的是,你必須專注在系統中最有可能改變的地方,來採用這個守則。
說到如此,你可能會從裝飾者模式這個名詞開始推想這是個什麼樣的架構!
我們先前瞭解了利用繼承無法完全解決問題,會造成類別的數量爆炸、設計死板以及基底類別加入的新功能並不適用於所有的子類別。
所以這邊所要採取的裝飾者模式,就是將以主食(漢堡、土司、蛋餅…etc)為主題,然後在執行期以配料(火腿、起司、玉米、生菜…etc)裝飾主食。
如果顧客想要火腿起司土司。那麼要作的就是:
1.拿一個土司的主食物件
2.以起司物件裝飾
3.以火腿物件裝飾
4.呼叫cost()方法,並依賴委派(delegate)將配料的價格加上去!!
你會看到如下圖!!
這代表什麼?!以下來分別說明
我們1.以土司物件開始,而這個土司物件可以繼承自先前所定義的早餐食物的超類別,因此也會有一個cost()方法用來計算食物的價格
當2.顧客想要在土司上加上起司,所以需要建立一個起司物件,並讓起司物件將土司包起來(當然,真實的食物是土司將起司包起來)
而這邊起司物件就是裝飾者,它的型態反映了它所裝飾的物件,這邊指的是早餐店的食物,也就是說,裝飾者與被裝飾者的物件型態是一致的。
所以起司也有一個cost方法,藉由多型,也可以把起司內所包含的早餐店的食物當成是早餐店的食物,也就是說
要讓起司有一個位置是在存放土司這個物件,而這個物件型態就是早餐店食物。
到了3,我們就知道,顧客也想要火腿,所以要建立一個火腿物件,將先前的起司物件(包含了土司物件)給包起來!
上述三者都是來自於早餐店物件,所以他們都具有cost的方法。
因此到了顧客買單、早餐店結帳的時候了!,我們只要呼叫最外圈的裝飾者的cost()方法,就可以辦得到。
而火腿的cost()就會先委由另一個物件,也就是起司物件計算到主食土司的價格,然後再加上起司的價格。
如下圖所示:
從剛剛物件之間的描述,可以歸納出裝飾者模式的意念!(定義可見原書)
1.裝飾者與被裝飾者有相同的超類別(土司與起司必須都是早餐店的食物類別下的子類別)
2.你可以利用一個或多個裝飾者包裝一個物件(土司被起司包裝,而起司又被火腿包裝)
3.我們在任何需要傳遞被包裝的物件時,改以傳遞裝飾後的物件。
4.裝飾者可以在所委派被裝飾者的行為,之前或之後加上自己的行為,達到特定的目的(cost()的方法,各個食物可以加上自己的價格傳回)
5.物件可以在任何時候被裝飾,所以可以在執行期動態地用任何適合的裝飾者來裝飾物件。
因此,隨著深入淺出設計模式這本書而言,裝飾者的定義為
動態地將責任加諸於物件上,若要擴充功能,裝飾者提供了比繼承更有彈性的選擇!
裝飾者模式定義下的類別圖如下:
簡單的說明如下:
1.具體的元件是動態加上新行為的物件,他擴充自元件
2.每個元件都可以單獨使用,或者是被裝飾者包起來使用
3.每一個裝飾者都有一個元件,也就是說裝飾者有一個實體變數參考到某個元件
4.抽象裝飾者是所有裝飾者共同實踐的介面,也可以真的宣告成抽象類別
5.裝飾者可以擴充元件的狀態,也可以加上新的方法,通常新的方法是基於舊的方法做一些變化
上述圖型說明了裝飾者的角色,但如何實際使用它呢?
我們套用到早餐店來看看!
你可能會想到,先前我們不是說要用合成取代繼承嗎?我還以為此模式中不會用到繼承…
事實上,裝飾者模式中的繼承行為,是為了讓裝飾者與被裝飾者必須是一樣的型態,也就是有共同的超類別。
這邊繼承是為了達到型態相符,而非取得”相同的行為”
然而,我們將裝飾者與元件合成時(例如將土司與起司合成時),就是加入新的行為,所得到的新行為就並非來自於超類別
而是由合成物件而來。”型態”是由繼承而來,但是行為卻是來自於裝飾者和基礎元件(食物),或者其他裝飾者之間的合成關係。
從執行期的動態角度,如果方法是依賴繼承,那麼類別的行為在編譯時就靜態地決定了,行為如果不是來自於超類別
就是來自於子類別的覆寫(override)。反之利用合成,可以在任何時候,實踐新的裝飾者來增加新的行為,不用修改到程式碼!
好的,裝飾者模式的精神,大概可以抓的到了,其實從結構來說也相當的單純,不過,通常在設計與實作仍存在著一些gap,
到底我們早餐店該如何來實作達到如此動態彈性的模式呢?(若仍嫌上面的說明不夠具體的話,可以看看程式碼)
1: //早餐超類別
2: public abstract class Breakfast
3: {
4: public string description = "未知";
5:
6: public virtual string getDescription() //"取得描述",在超類別已經實踐
7: {
8: return description;
9: }
10:
11: public abstract double cost(); //次類別才實踐
12:
13: }
ps.為了讓子類別(配料的抽象類別)去覆寫取得描述這個已實踐的方法,請加上virtual的字眼。
別忘了還有一個抽象的裝飾者類別要定義一下
1: //此為抽象的裝飾者類別,這是為了讓配料裝飾者這個類別能夠取代早餐超類別
2: public abstract class CondimentDecorator : Breakfast //配料的抽象類別繼承自早餐類別
3: {
4: public abstract override string getDescription();
5: //所有的配料裝飾者都必須重新實踐"取得描述"這個方法
6: //稍後解釋為什麼...
7: }
ps.這邊一定要加上override喲,不然在new配料抽象類別的子類別實例時,就會取得父類別的方法,導致程式邏輯錯誤
接著寫下各個主食(漢堡、土司等)的程式碼:
1: //首先先讓土司擴充早餐食物的類別,因為土司是早餐主食的一種
2: public class toast : Breakfast
3: {
4: public toast()
5: {
6: base.description = "土司"; //這邊的實體變數是繼承自超類別
7: }
8:
9: public override double cost()
10: {
11: return 10; //現在不需要管配料(生菜、起司)的價格,直接把土司的錢傳回即可
12: }
13: }
14:
15: public class burger : Breakfast
16: {
17: public burger()
18: {
19: base.description = "漢堡"; //這邊的實體變數是繼承自超類別
20: }
21:
22: public override double cost()
23: {
24: return 15; //現在不需要管配料(火腿、玉米)的價格,直接把漢堡的錢傳回即可
25: }
26: }
27: //培果與蛋餅可依上兩例實作程式碼。
輪到裝飾者配料的程式碼實踐了!
1: //寫各種配料的程式碼
2: //實踐具體的裝飾者
3: public class cheese : CondimentDecorator //起司是一個裝飾者,所以繼承自抽象的裝飾者類別
4: {
5: Breakfast breakfast;
6:
7: public cheese(Breakfast b)
8: {
9: breakfast = b;
10: }
11:
12: public override string getDescription()
13: {
14: return breakfast.getDescription() + ",起司";
15: }
16:
17: public override double cost()
18: {
19: return 5 + breakfast.cost();
20: }
21: }
22:
23:
24:
25: public class ham : CondimentDecorator //火腿是一個裝飾者,所以繼承自抽象的裝飾者類別
26: {
27: Breakfast breakfast;
28:
29: public ham(Breakfast b)
30: {
31: breakfast = b;
32: }
33:
34: public override string getDescription()
35: {
36: return breakfast.getDescription() + ",火腿";
37: }
38:
39: public override double cost()
40: {
41: return 10 + breakfast.cost();
42: }
43: }
44: //玉米或生菜、其他配料,可依上兩例實作程式碼。
45:
46:
47:
時候到了!客人上門了,且來看看利用裝飾者模式如何設計出靈活程式吧
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: //記得,是為超類別寫程式,而不是為次類別寫程式喲
6: //客人訂了一份土司,不需要配料,列出他的價格與描述
7: Breakfast breakfast1 = new toast();
8: Console.WriteLine("餐點:{0} 價格:{1}",breakfast1.getDescription(),breakfast1.cost());
9:
10: //客人訂了一份土司,要加"雙倍"火腿,列出他的價格與描述
11: Breakfast breakfast2 = new toast();
12: breakfast2 = new ham(breakfast2); //用第一個火腿來裝飾它
13: breakfast2 = new ham(breakfast2); //用第二個火腿來裝飾它
14: Console.WriteLine("餐點:{0} 價格:{1}", breakfast2.getDescription(), breakfast2.cost());
15:
16:
17:
18: //客人訂了一份漢堡,要加火腿跟雙倍起司,列出他的價格與描述
19: Breakfast breakfast3 = new burger();
20: breakfast3 = new ham(breakfast3); //用火腿來裝飾它
21: breakfast3 = new cheese(breakfast3); //用起司來裝飾它
22: breakfast3 = new cheese(breakfast3); //用起司來裝飾它
23: Console.WriteLine("餐點:{0} 價格:{1}", breakfast3.getDescription(), breakfast3.cost());
24:
25:
26: Console.ReadKey();
結果是如何呢?
正如同之前所說的裝飾者模式般的運作,你可以看到我們的合成行為都是在執行期所配置的。若有仔細看,都會發現目前的設計模式
都是朝向彈性(可以在執行期動態改變),而且修改範圍達到最小,甚至是使用者根本不需要知道裝飾者的存在(封裝的威力)
我們可以漸漸的發現Design Pattern的一些精神與要旨。
不過雖然裝飾者模式很有趣,也很有魅力(如同前幾個),不過也有一些缺點,本書提到,裝飾者會導致程式中出現許多小類別(例如:起司、生菜..etc)
如果過度使用,會讓程式變得很複雜!以早餐店的例子再多加幾個配料就夠複雜了!所以還必須思考這個模式的優缺點,使用在適合的地方
這不就是我們高級知識份子的工作嗎…(不是絕對的是非題,而要有辨別何時該做何時不該做)
後記:
定義來自於深入淺出設計模式,為了理解,我試著去想一個生活上的例子
這個模式看起來很簡單,實作上的篇幅也滿簡潔的,不過想這個例子就覺得不夠完美,這邊也只能說,雖然這個例子也符合原書上的例子
可以彈性的去想象,不過,假如要追求完美一點的例子的話,一定會有一些bug的啦(比如說,配料:蛋與主食:蛋餅,到底要怎麼切割)
不過既然是例子,就別想這麼多,在我所舉出來的情境中,至少還沒有太大的衝突就是了。
有要注意的就是”抽象的裝飾者類別”,因為他有這樣的一個”抽象”角色
所以在他的類別性質上要設定好,不然像我看原文的時候,例如,java override不用標明,所以會有所搞混,程式碼也有一點不一樣
若照著寫都跑不太出來結果,要真的去思考他的抽象角色,對應正確的類別性質。
後來百番測試之後,發現了這些小小的差異也算一種收獲吧,終於得到了所要的結果
證明了裝飾者模式的實作跟趣味性,而且這個設計模式非常具有彈性。書上提到了java函式庫中的i/o相關
API類別其實也是透過裝飾者模式來達到讀取檔案的效果,若想要看到進一步的說明,就去看原文吧!!