裝飾者模式筆記

裝飾者模式筆記

我們常常看到早餐店的蹤跡,每個街頭巷角幾乎都有一家早餐店在營業。

早餐店可以說是跟杯裝飲料一樣進入門檻極低的行業之一。

 

為了跟市面上的傳統早餐店競爭,現在有一家早餐店即將開啟,

適逢創業階段,他們的早餐店只賣了四樣主食(飲料在此先省略以單純的思考問題的架構,在未來可以進一步針對飲料店或是其他類型的商店作構思)

因為他們的材料手藝真的不錯,所以開市即熱賣

他們目前導入了訂單系統來管理他們的早餐的物料與營收等資訊。

他們開創期遵循著”少樣多量”的原則來處理他們的早餐商品,但是他們目前希望可以開始擴展他們的品牌與商品多樣化。

 

他們原先的銷售系統設計如下:

pic1

(註:先別問我這張圖的正當性,至少剛開始只有這四種主食就對了)

 

而上面這張圖是的結構是這樣:”早餐店的食物(菜單)”是一個抽象類別,店內所提供的食物都必須繼承自此類別。

當中的cost方法是抽象的,所以子類別也必須定義自己的cost實踐,每個子類別食物都要實踐cost()來告知整份早餐的價格。

 

因為目前已經累積了一定的客源與資金,所以這家早餐店準備更新他們訂單系統,來合乎他們新開發出來的早餐菜單。

滿足與吸引他們的早餐顧客,他們打算以彈性的方式提供客人選擇自己想吃的配料,加到不同種類的早餐食物中。

 

我們可以想像購買早餐時,可以要求各種加料,例如,漢堡我想要加蛋、生菜、雙倍肉,蛋餅也可以加培根、玉米,甚至是都加!!。

不過這些對於早餐店來說這些都是成本,所以早餐店會根據加料酌收費用。所以銷售系統必須考量到這些加料的部分。

這是可能是我們嘗試設計的第一個版本!!

如下圖

pic2

哇咧!!這簡直是類別爆炸!!別笑這個版本架構,其實我們看菜單上,往往也都是這樣設計

例如,漢堡加蛋30元,漢堡不加蛋25元,雞排堡35元,都是重覆地顯示在單子上呢!(所以有早餐的單子就很大張,而且字還很小)

正如同上圖,每一個子類別的食物組合,都會透過cost()的方法將計算出主食加上各種佐料的價格。

 

不過,身為設計模式學習者,我們必須來思考這樣的結構!

從先前的OO守則,很明顯著,這樣的模式設計會遇到維護上的困難。如果起司的價格上揚怎麼辦?!要加入一個新的生菜配料時,怎麼辦??

造成了這種維護上的困難,違反了我們先前提過的兩個設計守則,而且很嚴重!

1.________________________

2.________________________

有興趣的去找找 http://www.dotblogs.com.tw/pin0513/category/3265.aspx

 

所以你可能會說:「笨透了,幹嘛設計這麼多類別呀?利用實體變數和繼承,就可以追蹤這些配料了呀!!」

所以我們可能可以這樣對早餐店食物的類別下手!!

pic3

這樣我們只需要五個類別!!

 

我們為每種配料加入布林值,有火腿與加火腿等方法則是取得和設定配料的布林值

而現在早餐店的食物類別中的cost方法,不再是一個抽象方法,在這邊必須去實踐它,必須讓他在這邊要加入佐料的價格。子類別仍將覆寫cost方法,但是會調用超類別的cost來計算出基本食物(漢堡、土司、貝果、蛋餅…etc)加上配料的價格。

整體類別圖就變成了:

pic4

而超類別實踐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)將配料的價格加上去!!

你會看到如下圖!!

pic5

這代表什麼?!以下來分別說明

我們1.以土司物件開始,而這個土司物件可以繼承自先前所定義的早餐食物的超類別,因此也會有一個cost()方法用來計算食物的價格

當2.顧客想要在土司上加上起司,所以需要建立一個起司物件,並讓起司物件將土司包起來(當然,真實的食物是土司將起司包起來)

而這邊起司物件就是裝飾者,它的型態反映了它所裝飾的物件,這邊指的是早餐店的食物,也就是說,裝飾者與被裝飾者的物件型態是一致的。

所以起司也有一個cost方法,藉由多型,也可以把起司內所包含的早餐店的食物當成是早餐店的食物,也就是說

要讓起司有一個位置是在存放土司這個物件,而這個物件型態就是早餐店食物。

到了3,我們就知道,顧客也想要火腿,所以要建立一個火腿物件,將先前的起司物件(包含了土司物件)給包起來!

上述三者都是來自於早餐店物件,所以他們都具有cost的方法。

因此到了顧客買單、早餐店結帳的時候了!,我們只要呼叫最外圈的裝飾者的cost()方法,就可以辦得到。

而火腿的cost()就會先委由另一個物件,也就是起司物件計算到主食土司的價格,然後再加上起司的價格。

如下圖所示:

4

 

從剛剛物件之間的描述,可以歸納出裝飾者模式的意念!(定義可見原書)

1.裝飾者與被裝飾者有相同的超類別(土司與起司必須都是早餐店的食物類別下的子類別)

2.你可以利用一個或多個裝飾者包裝一個物件(土司被起司包裝,而起司又被火腿包裝)

3.我們在任何需要傳遞被包裝的物件時,改以傳遞裝飾後的物件。

4.裝飾者可以在所委派被裝飾者的行為,之前或之後加上自己的行為,達到特定的目的(cost()的方法,各個食物可以加上自己的價格傳回)

5.物件可以在任何時候被裝飾,所以可以在執行期動態地用任何適合的裝飾者來裝飾物件。

 

因此,隨著深入淺出設計模式這本書而言,裝飾者的定義為

動態地將責任加諸於物件上,若要擴充功能,裝飾者提供了比繼承更有彈性的選擇!

裝飾者模式定義下的類別圖如下:

pic7

簡單的說明如下:

1.具體的元件是動態加上新行為的物件,他擴充自元件

2.每個元件都可以單獨使用,或者是被裝飾者包起來使用

3.每一個裝飾者都有一個元件,也就是說裝飾者有一個實體變數參考到某個元件

4.抽象裝飾者是所有裝飾者共同實踐的介面,也可以真的宣告成抽象類別

5.裝飾者可以擴充元件的狀態,也可以加上新的方法,通常新的方法是基於舊的方法做一些變化

 

上述圖型說明了裝飾者的角色,但如何實際使用它呢?

我們套用到早餐店來看看!

pic8

你可能會想到,先前我們不是說要用合成取代繼承嗎?我還以為此模式中不會用到繼承…

事實上,裝飾者模式中的繼承行為,是為了讓裝飾者與被裝飾者必須是一樣的型態,也就是有共同的超類別。

這邊繼承是為了達到型態相符,而非取得”相同的行為”

 

然而,我們將裝飾者與元件合成時(例如將土司與起司合成時),就是加入新的行為,所得到的新行為就並非來自於超類別

而是由合成物件而來。”型態”是由繼承而來,但是行為卻是來自於裝飾者和基礎元件(食物),或者其他裝飾者之間的合成關係。

從執行期的動態角度,如果方法是依賴繼承,那麼類別的行為在編譯時就靜態地決定了,行為如果不是來自於超類別

就是來自於子類別的覆寫(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();

結果是如何呢?

pic9

正如同之前所說的裝飾者模式般的運作,你可以看到我們的合成行為都是在執行期所配置的。若有仔細看,都會發現目前的設計模式

都是朝向彈性(可以在執行期動態改變),而且修改範圍達到最小,甚至是使用者根本不需要知道裝飾者的存在(封裝的威力)

我們可以漸漸的發現Design Pattern的一些精神與要旨。

 

不過雖然裝飾者模式很有趣,也很有魅力(如同前幾個),不過也有一些缺點,本書提到,裝飾者會導致程式中出現許多小類別(例如:起司、生菜..etc)

如果過度使用,會讓程式變得很複雜!以早餐店的例子再多加幾個配料就夠複雜了!所以還必須思考這個模式的優缺點,使用在適合的地方

這不就是我們高級知識份子的工作嗎…(不是絕對的是非題,而要有辨別何時該做何時不該做)

 

後記:

定義來自於深入淺出設計模式,為了理解,我試著去想一個生活上的例子

這個模式看起來很簡單,實作上的篇幅也滿簡潔的,不過想這個例子就覺得不夠完美,這邊也只能說,雖然這個例子也符合原書上的例子

可以彈性的去想象,不過,假如要追求完美一點的例子的話,一定會有一些bug的啦(比如說,配料:蛋與主食:蛋餅,到底要怎麼切割)

不過既然是例子,就別想這麼多,在我所舉出來的情境中,至少還沒有太大的衝突就是了。

 

有要注意的就是”抽象的裝飾者類別”,因為他有這樣的一個”抽象”角色

所以在他的類別性質上要設定好,不然像我看原文的時候,例如,java override不用標明,所以會有所搞混,程式碼也有一點不一樣

若照著寫都跑不太出來結果,要真的去思考他的抽象角色,對應正確的類別性質。

後來百番測試之後,發現了這些小小的差異也算一種收獲吧,終於得到了所要的結果

證明了裝飾者模式的實作跟趣味性,而且這個設計模式非常具有彈性。書上提到了java函式庫中的i/o相關

API類別其實也是透過裝飾者模式來達到讀取檔案的效果,若想要看到進一步的說明,就去看原文吧!!