示例
對於裝飾器模式,我想先不談概念,而是先從一個例子開始說起,看看面對這樣的需求,我們應該如何處理,並希望由此逐步引出裝飾器模式以加深理解。
需求
假設現在需要開一個奶茶店,奶茶種類繁多,如紅豆奶茶,布丁奶茶,珍珠奶茶,紅豆珍珠奶茶等。種類雖多,但實質上都是在奶茶中加了各種配料而已。為了簡化實現,繼續假設奶茶的價格根據奶茶本身加上不同配料累計計算而成。然後,根據每個客戶的要求,每種奶茶又可以加糖或者加冰,加糖加冰不額外收費。
初級方案
在學習設計模式之前,或許最容易想到的方案就是繼承了,即先定義奶茶類,然後再定義各種奶茶子類繼承自奶茶類,考慮到以後或許還會有更多的飲品,例如咖啡,因此再定義一個飲品的抽象基類,讓奶茶類繼承自飲品基類,這樣一來,最終設計可能會如下類圖所示:
部分程式碼如下:
public abstract class Drink
{
public string Name { get; set; }
public int Price { get; set; }
public abstract string Desc { get; }
public abstract int Cost { get; }
}
public class Naicha : Drink
{
public Naicha()
{
Name = "奶茶";
Price = 8;
}
public override string Desc => this.Name;
public override int Cost => this.Price;
}
public class HongDouNaicha : Naicha
{
public HongDouNaicha()
{
Name += "+紅豆";
Price += 1;
}
}
public class ZhenzhuNaicha : Naicha
{
public ZhenzhuNaicha()
{
Name += "+珍珠";
Price += 3;
}
}
...
問題
不難想象,這種設計是一種災難,因為它至少會出現如下四個問題:
- 類爆炸,程式碼雖然只列了部分,但通過類圖可以看出類的數量必定會達到一個恐怖的地步;
- 如果加冰改為收費,需要多處修改價格,程式碼維護困難,嚴重違反開閉原則;
- 如果新增配料,類的數量會急劇增加,程式碼維護困難,嚴重違反開閉原則;
- 無法實現加多份配料,如多冰、多糖等。
改進一
由於上述問題,現實促使我們不得不對方案進行改進,不過好在對於類爆炸的問題,我們是有經驗的,我們在學習工廠方法模式的時候就出現過類爆炸,我們通過合併的方式就演化出了抽象工廠模式,這裡我們也可以依葫蘆畫瓢,對類進行合併。
合併後的類圖如下:
再看看程式碼:
public abstract class Drink
{
public string Name { get; set; }
public int Price { get; set; }
public abstract string Desc { get; }
public abstract int Cost { get; }
public abstract void AddBuding();
public abstract void AddHongdou();
public abstract void AddZhenzhu();
public abstract void AddBing();
public abstract void AddTang();
}
public class Naicha : Drink
{
private string _desc = string.Empty;
private int _cost = 0;
public Naicha()
{
Name = "奶茶";
Price = 8;
}
public override string Desc => this.Name + _desc;
public override int Cost => this.Price + _cost;
public override void AddBing()
{
_desc += "+冰";
}
public override void AddBuding()
{
_desc += "+布丁";
_cost += 2;
}
public override void AddHongdou()
{
_desc += "+紅豆";
_cost += 1;
}
public override void AddTang()
{
_desc += "+糖";
}
public override void AddZhenzhu()
{
_desc += "+珍珠";
_cost += 3;
}
}
優點
將各種子類都直接改成抽象方法放到Drink父類中,效果簡直立杆見影,起碼解決了初級方案中的兩個問題:
- 消除了類爆炸的問題,程式碼簡潔,一下子就只剩下兩個類了;
- 配料可以任意搭配組合,並且也可以加入多份。
缺點
但是新的問題也隨之而來:
- 如果修改價格或新增配料就需要新增方法,違反了開閉原則;
- 如果新增飲品咖啡,這時也會變得麻煩,因為咖啡需要冰和糖,同時還需要咖啡伴侶,但是不需要布丁、珍珠、紅豆等。
可以看到,增加了咖啡之後,父類以及每個子類的程式碼都要跟著修改,而且每個子類都必須繼承大量無用的方法。public abstract class Drink { ... public abstract void AddKafeibanlv(); } public class Naicha : Drink { ... public override void AddKafeibanlv() { } } public class Kafei : Drink { ... public override void AddBuding() { } public override void AddHongdou() { } public override void AddZhenzhu() { } public override void AddKafeibanlv() { _desc += "+咖啡伴侶"; _cost += 2; } }
改進二
因此,我們還需要進一步改進,這次我們改進的方向是將這些方法抽象併合並,因為我們可以看到,上面的方案之所以會有這麼多問題就是因為面向了實現程式設計,每個方法都代表了一種配料,如果我們將這些配料全部繼承自同一個抽象類,然後提供一個面向抽象的AddPeiliao(Peiliao peiliao)
方法不就可以這個問題了嗎?於是我們就有了如下改進:
為了滿足這個需求,我們對飲品基類也進行了較大的改造,程式碼如下:
public abstract class Drink
{
protected List<Peiliao> Peiliaos = new List<Peiliao>();
public string Name { get; set; }
public int Price { get; set; }
public int Cost
{
get
{
int cost = this.Price;
foreach (var peiliao in Peiliaos)
{
cost += peiliao.Price;
}
return cost;
}
}
public string Desc
{
get
{
string desc = this.Name;
foreach (var peiliao in Peiliaos)
{
desc += "+" + peiliao.Name;
}
return desc;
}
}
public void AddPeiliao(Peiliao peiliao)
{
Peiliaos.Add(peiliao);
}
}
public class Naicha : Drink
{
public Naicha()
{
Name = "奶茶";
Price = 8;
}
}
由於配料全部通過一個集合組合到了基類中,因此,不需要通過抽象方法讓子類計算價格,而是直接在基類中迴圈疊加計算,同時,由於大部分的功能都在基類中實現了,子類變得乾淨簡潔了。
再看看配料:
public abstract class Peiliao
{
public abstract string Name { get; }
public abstract int Price { get; }
}
public class Buding : Peiliao
{
public override string Name => "布丁";
public override int Price => 2;
}
同樣的簡潔乾淨。
優點
這樣的改進優點是巨大的,幾乎解決了所有問題:
- 配料可任意搭配組合,並且滿足新增飲品的需求;
- 新增飲品和配料均只需要增加新的類即可,滿足開閉原則
缺點
感覺上好像挺不錯的,堪稱完美!難道這就是今天的主角---裝飾器模式?其實,我們忽略了兩個問題:
- 這個方案以及上一個方案都犯了一個致命的錯誤,就是修改了飲品類,這在很多時候是不被允許的,或者說根本做不到的,就好比我們要給手機加個裝飾---貼個膜,難道我們要先改一下手機的內部結構嗎?這明顯是不合理,也是做不到的。
Add
方法也不太合理,飲料不應該具有新增配料的能力,這好比給了手機一個膜,手機自己貼上了,總覺得哪裡怪怪的。
改進三
其實,從設計原則的角度來講,上一個方案的改進已經很接近了,因為它已經滿足了開閉原則,擴充套件性方面也非常優秀,唯一的問題就是需要修改奶茶類,這通常是不能實現的。那麼,我們思路再次轉變一下,奶茶類不能改,但是配料可以改啊,我們換個依賴方向,將奶茶聚合到配料中不就可以了嗎?於是就有了如下類圖:
再看看程式碼:
public abstract class Drink
{
public string Name { get; set; }
public int Price { get; set; }
public abstract string Desc { get; }
public abstract int Cost { get; }
}
飲品類還原到了最初狀態,沒做任何修改。
public class Peiliao:Drink
{
protected readonly Drink Drink;
public Peiliao(Drink drink)
{
Drink = drink;
}
public override string Desc
{
get
{
return Drink.Desc + "+" + this.Name;
}
}
public override int Cost
{
get
{
return Drink.Cost + this.Price;
}
}
}
將飲品類聚合到了配料類中,但是這裡和前一個方案又有所不同,因為配料畢竟是配料,聚合方向換了之後,通過new
就只能得到配料而得不到奶茶了,因此,為了最終能得到奶茶,我們的配料也必須繼承自飲品類,這看起來很怪,但妙也妙在這裡,通過聚合+繼承的方式改進,可使得飲品的擴充套件更靈活,同時也遵守了開閉原則。其中,聚合是為了實現功能,而繼承是為了約束型別,這就是裝飾者模式。
定義
裝飾器模式動態地給一個物件增加一些額外的職責。就增加功能而言,裝飾器模式比生成子類更為靈活。
UML類圖
優缺點
優點
- 可動態的給一個物件增加額外的職責
- 有很好地可擴充套件性
缺點
- 增加了程式的複雜度,剛接觸理解起來會比較困難
跟代理模式的區別
裝飾器模式跟代理模式類圖十分相似,但是,它們之間卻有很大的區別:
- 裝飾器模式關注於在一個物件上動態的新增方法,而代理模式關注於控制對物件的訪問。
- 裝飾器模式通常用聚合的方式,而代理模式通常採用組合的方式。
- 裝飾器模式通常會套用多層,而代理模式通常只有一層。
但是由於他們的結構十分相似,因此很多時候二者可以做同樣的事,比如裝飾器模式和代理模式都可用於實現AOP(面向切面程式設計)。
經典案例
在.NET類庫中,System.IO.Stream
就是裝飾者模式的一個經典案例,不過在這個案例中沒有用到Decorator基類。
總結
裝飾器模式可以說是結構型設計模式的巔峰之作,其中設計思想十分精妙,但理解起來也確實有些困難,因此,可能還是需自己動手擼碼,加深體會。