相信大家都在都在漢堡店吃過漢堡,有些漢堡店很有特色,推出了漢堡訂製服務,即,可以在漢堡中加料,加肉餅,加生菜之類(有點類似我們本地的肥腸粉裏面加冒結子)。更是讓不少吃貨大快朵頤,大呼過癮,加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主要用於如下場景:
- 想要方便的添加一些行爲,而這些行爲又不屬於類的核心行爲。
- 添加行爲的時候,不希望出現類數量爆炸的時候。