[Design Pattern] 裝飾者模式 (Decorate Pattern) 義大利麵餐館

摘要:[Design Pattern] 裝飾者模式 (Decorate Pattern)

前言


  這次要介紹的是裝飾者模式,由於目前便當這話題還蠻紅的,這次的裝飾者模式就用吃的來舉例吧XD,一般我們去餐廳時拿到菜單時都會發現菜單裡有許多種類的菜色可以選擇,以義大利麵為例就可能有臘腸義大利麵、茄汁青醬蛤蠣麵、義式風藏火腿麵、百里香嫩雞肉麵等等。但是仔細注意一下,其實這些多樣式的菜色都是 (主菜+配料+麵) 的組合而成,那這種組合跟裝飾者模式有什麼關係呢? 讓我們繼續看下去。

 

首先我們試著將一個義大利麵餐廳的訂餐系統拉成一個類別圖來看看。

  由以上這個類別圖可以看出每一種的菜色各自實作自己的類別並且都繼承於 Spaghetti 類別後覆寫 price() 方法定價,因為各種菜色不同而使用的佐料也不相同,接下來如果餐廳的菜色繼續增加的時候會發生什麼情形呢? 看看下面的類別圖就能夠瞭解了。

  是不是感覺到非常的凌亂呢? 每當一種新佐料的加入或改變成一種新菜色時,就要產生一個對應於佐料的類別來處理,這樣的作法將導致日後類別變成龐大且難以維護,為了避免這種情況的發生,就可以使用裝飾者模式的觀念來設計。

 

  而什麼是裝飾者模式的概念呢? 以自助餐的概念來舉例,在自助餐的選菜模式下,每種的配菜都是先處理好放置在桌上,當你想吃青菜時就去夾青菜、想吃滷肉時就去夾滷肉,也就是需要什麼配菜就去夾哪種配菜,最後將拼裝成包含主菜+配菜+白飯的一份餐點,透過「裝飾」的概念來達到想要什麼就附加什麼上去而不需要的自然不用去理會,接下來就讓我們開始重新設計這個類別,看看以「裝飾」的方式會產生什麼樣的結果。

 

範例


  一個裝飾的運作應該是什麼樣子呢? 首先看以下的運作,一開始我定義了一個 NormalSpaghetti 類別,NormalSpaghetti 類別繼承至 Spaghetti 類別,所以包含了 Spaghetti 類別的屬性與方法。

  接下來我希望添加碎肉在這個義大利麵上,所以我建立了一個 MincedBeef 類別,MincedBeff 是一個裝飾類別他將 NormalSpaghetti 類別包含在此類別中。

  再來還需要來點番茄添加風味,所以建立了 Tomato 類別,將以上兩個物件包含在內。

  最後再來點洋蔥,建立了 Onion 類別,如此就完成了我的 番茄肉醬義大利麵了!

  好了,菜色是完成了但是價格要如何去計算呢? 參考上圖從義大利麵開始到後面附加上的配料物件之中都包含了 Price() 方法,當如不同的配料透過 Price() 方法我們可以指定此配料應該要多少錢,透過一層一層的包裝下在每個 Price() 方法中額外的加入該配料的價錢後再回傳,如下。

整個流程如下

  1. 最外層呼叫了 Onion 的 Price() 方法
  2. Onion 呼叫了 Tomato 的 Price() 方法
  3. Tomato 呼叫了 MincedBeef 的 Price() 方法
  4. NormalSpaghetti 回傳了本身的初始定價給 MincedBeff
  5. MincedBeff 將 NormalSpaghetti 回傳的價錢+50後回傳給 Tomato
  6. Tomato 將 MincedBeff 回傳的價錢+30後回傳給 Onion
  7. Onion 將 Tomato 回傳的價錢+20後回傳給最外層的呼叫

也就是裝飾者能夠在所委派的裝飾者行為之前或之後附加自己的行為而達到目的。

瞭解了裝飾者應處理的行為後,接著就讓我們把重新設計的義大利麵餐館訂餐類別圖畫出來,如下。

  由上圖可知,NormalSpaghetti 是擴充自 Spaghetti 的一個行為,CondimentDecorator 是裝飾者共同實踐的抽象類別,MincedBeef、Bacon、Cream等等則為擴充的裝飾者,在裝飾者中還需要有 spaghetti 變數能夠紀錄所要裝飾的對象,接著繼續來看看程式應該如何撰寫。

public abstract class Spaghetti
{
    public string name = "未知名稱"; 
    public string description = "未知配料";

    public abstract int GetPrice();

    public virtual string GetName()
    {
        return this.name;
    }

    public virtual string GetDescription()
    {
        return this.description;
    }
}

Spaghetti 類別將 GetPrice() 方法宣告為抽象方法將於次類別去實作,GetName() 與 GetDescription() 則在此先行實作。

public abstract class CondimentDecorator : Spaghetti
{
}

CondimentDecorator 類別在此只單存提供一個裝飾者使用的父類別,但是還是需要繼承 Spaghetti 類別。

public class NormalSpaghetti : Spaghetti
{
    public override string GetName()
    {
        return "平民義大利麵";
    }

    public override string GetDescription()
    {
        return "義大利細麵";
    }

    public override int GetPrice()
    {
        return 100;
    }
}

NormalSpaghetti 類別中覆寫了 GetName()、GetDescription()、GetPrice() 方法,提供了初始化的數值。

public class Ham : CondimentDecorator
{
    Spaghetti spaghetti;

    public Ham(Spaghetti pSpaghetti)
    {
        this.spaghetti = pSpaghetti;
    }

    public override string GetDescription()
    {
        return spaghetti.GetDescription() + ",火腿";
    }

    public override int GetPrice()
    {
        return spaghetti.GetPrice() + 38;
    }

    public override string GetName()
    {
        return spaghetti.GetName();
    }
}

public class Egg : CondimentDecorator
{
    Spaghetti spaghetti;

    public Egg(Spaghetti pSpaghetti)
    {
        this.spaghetti = pSpaghetti;
    }

    public override string GetDescription()
    {
        return spaghetti.GetDescription() + ",蛋";
    }

    public override int GetPrice()
    {
        return spaghetti.GetPrice() + 5;
    }

    public override string GetName()
    {
        return spaghetti.GetName();
    }
}

public class Cheese : CondimentDecorator
{
    Spaghetti spaghetti;

    public Cheese(Spaghetti pSpaghetti)
    {
        this.spaghetti = pSpaghetti;
    }

    public override string GetDescription()
    {
        return spaghetti.GetDescription() + ",起司";
    }

    public override int GetPrice()
    {
        return spaghetti.GetPrice() + 20;
    }

    public override string GetName()
    {
        return spaghetti.GetName();
    }
}

  Ham、Egg、Cheese 類別為裝飾者類別,在裝飾者類別的建構子中會傳入要裝飾的目標並儲存於 spaghetti 變數中,並且於此類別中覆寫 GetName()、GetDescription()、GetPrice() 方法加入了自己的行為後再回傳處理完的結果,實際執行參考以下程式碼。

static void Main(string[] args)
{
    Console.WriteLine("----義大利餐館菜單----");

    Spaghetti normalSpaghetti = new NormalSpaghetti(); // 產生平民義大利麵
    normalSpaghetti = new Ham(normalSpaghetti); // 加點火腿
    normalSpaghetti = new Egg(normalSpaghetti); // 加點蛋
    normalSpaghetti = new Cheese(normalSpaghetti); // 加點起司
    Console.WriteLine("名稱:{0} 價錢:{1} 材料:{2}", 
        normalSpaghetti.GetName(), 
        normalSpaghetti.GetPrice(), 
        normalSpaghetti.GetDescription());

    Spaghetti spaghettiBolognese = new SpaghettiBolognese(); // 產生茄汁肉醬義大利麵
    spaghettiBolognese = new MincedBeef(spaghettiBolognese);
    spaghettiBolognese = new Tomato(spaghettiBolognese);
    spaghettiBolognese = new Garlic(spaghettiBolognese);
    Console.WriteLine("名稱:{0} 價錢:{1} 材料:{2}",
        spaghettiBolognese.GetName(), 
        spaghettiBolognese.GetPrice(), 
        spaghettiBolognese.GetDescription());

    Console.ReadLine();
}

  當有新口味的義大利麵要推出時,即只需要建立該口味義大利麵物件再依據要搭配的配料進行包裝,如以上程式碼的做法中先產生了 NormalSpaghetti 物件並且透過了 Ham、Egg、Cheese 物件的包裝後最後再呼叫該方法,即能夠依照所包裝的物件一層一層的呼叫並依據各裝飾者物件內部的額外添加動作處理後再回傳結果,當日後日需要切換配料時只需要將裝飾者物件稍作變動即可,就不用額外再去增加多餘的類別了。

以上就是這次裝飾者模式的介紹。

 

範例程式碼 2016/09/24 補檔


https://drive.google.com/open?id=0B40daTESrAXwQXA4MVhsYjVCVlE

 

參考資料


Head First Design Pattern 深入淺出設計模式




以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)