從漢堡加料說起——淺談C#中的Decorator模式

相信大家都在都在漢堡店吃過漢堡,有些漢堡店很有特色,推出了漢堡訂製服務,即,可以在漢堡中加料,加肉餅,加生菜之類(有點類似我們本地的肥腸粉裏面加冒結子)。更是讓不少吃貨大快朵頤,大呼過癮,加6,7層肉餅的感覺簡直不要太好。
 
那麼大飽口福之後,讓我們來思考一個問題,漢堡是要錢的,加的料,比如肉餅,生菜,也都是收費的,如果讓我們來設計出一套類,計算客戶買漢堡的消費,我們應該怎麼做比較合適?這裏爲了簡單起見,我們就假定加的肉餅是beef,生菜是tomatto。
 

第一種設計

建立3個類,一個表示漢堡,另外兩個表示肉餅和生菜。漢堡類中有辦法添加肉餅和生菜。結算費用的時候,直接調用漢堡類的方法。

在代碼中則以這樣的形式呈現。

    class Beef
    {
        public double GetCost()
        {
            return 10;
        }
    }

    class Tomatto
    {
        public double GetCost()
        {
            return 5;
        }
    }
    class Hamburg
    {
        public List<Beef> Beefs { get; private set; } = new List<Beef>();
        public List<Tomatto> Tomattos { get; private set; } = new List<Tomatto>();

        public void AddBeef(Beef beef)
        {
            Beefs.Add(beef);
        }

        public void AddTomatto (Tomatto tomatto)
        {
            Tomattos.Add(tomatto);
        }

        public double GetCost()
        {
            var result = 20d; //hamburg's cost
            Beefs.ForEach(b => result += b.GetCost());
            Tomattos.ForEach(t => result += t.GetCost());

            return result;
        }
    }

這就是最簡單的一種實現方法,然而它有以下幾個弊端。

  • 類數量過多,應該通過抽象減少類數量,如果以後還有雞肉餅,小龍蝦餅,豈不是又要加新的類?而其實這些類彼此都是相似的。
  • 不滿足開閉原則。如果以後有了其他可以添加的料,我們會不可避免的修改Hamburg類。
  • Hamburg類與具體的料耦合。

所以,這種最簡單的做法,如果對於一個小項目或者很簡單的案例,我們還可以容忍,如果對於一個大項目,或者預計到未來會出現需求改變的時候,我們就需要改進我們的設計方案。
 

第二種設計,抽象出料接口

第一種設計中很大的一個缺陷來自於,不管是牛肉餅也罷,生菜也罷,它們都漢堡的一種添加物,對於計費系統,關心的也僅僅是添加物的名字和價格而已,所以,我們應該抽象出接口來進行漢堡類和具體添加物類的解耦。

代碼實現如下:

    interface Addin
    {
        double GetCost();
    }

    class Beef:Addin
    {
        public double GetCost()
        {
            return 10;
        }
    }

    class Tomatto:Addin
    {
        public double GetCost()
        {
            return 5;
        }
    }

    class Hamburg
    {
        public List<Addin> Addins { get; private set; } = new List<Addin>();
        public void AddAddin(Addin addin)
        {
            Addins.Add(addin);
        }

        public double GetCost()
        {
            var result = 20d;
            Addins.ForEach(a => result += a.GetCost());
            return result;
        }
    }

在第二版設計中,我們提煉出了接口addin,使得hamburg類依賴於addin而不直接依賴於具體某個添加物類。同時也保證了開閉原則的實現,就算新的添加物上線,我們也不用修改hamburg類了,我們似乎達到了設計的理想境界。
但是真的這樣就無懈可擊了嗎?
 

第三種設計,Decorator模式

雖然我們第二種設計解決了依賴於具體類的問題並實現了開閉原則,但是還是會有人覺得不爽,因爲大家覺得,雖然第二種設計沒有什麼大問題了,但是在語義上面,我們希望能保證hamburg類的純潔性。什麼意思呢,就是說,hamburg自己代表自己的價格就行了,添加物畢竟是外來物,沒有必要深入到hamburg類的內部。
所以,我們就再次更新我們的設計,這次我們祭出Decorator模式。

以下是Decorotor模式中需要注意的點:

  • 裝飾類基類和被裝飾對象都繼承自同一個接口,裝飾基類內部還聚合了一個此接口對象。
  • 裝飾具體類在計算中,先計算自己那部分,再調用基類方法,基類方法一般是計算內部聚合的那個對象, 這樣確保了裝飾模式可以一層嵌套一層。

我們看看具體代碼。

    abstract class Food
    {
        public abstract double GetCost();
    }

    class Hamburger : Food
    {
        public override double GetCost()
        {
            return 20;
        }
    }

    class FoodDecorate : Food
    {
        private Food _food = null;
        public FoodDecorate(Food food)
        {
            _food = food;
        }

        public override double GetCost()
        {
            return _food.GetCost();
        }
    }

    class TomatoDecorator : FoodDecorate
    {
        public TomatoDecorator(Food food) : base(food) { }
        public override double GetCost()
        {
            return 5 + base.GetCost();
        }
    }

    class BeefDecorator : FoodDecorate
    {
        public BeefDecorator(Food food) : base(food) { }
        public override double GetCost()
        {
            return 10 + base.GetCost();
        }        
    }

因爲不管是Hamburg還是Decorator,大家都實現了Food接口,同時Decorator聚合的也是Food對象,所以在客戶端我們可以很方便的寫

    BeefDecorator beefAddHamburg = new BeefDecorator(new BeefDecorator(new Hamburger()));
    Console.WriteLine(beefAddHamburg.GetCost());

以此來表示加了兩層牛肉的hamburg。怎麼樣,這是不是比第二種設計又方便了一點呢?
 
總結一下,Decorator主要用於如下場景:

  • 想要方便的添加一些行爲,而這些行爲又不屬於類的核心行爲。
  • 添加行爲的時候,不希望出現類數量爆炸的時候。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章