3. 裝飾者模式:裝飾物件
3.1. 引言: Es什麼so?Cappu什麼no?
你會不會忽然地出現,在街角的咖啡店……
喂喂,喝咖啡本來是為了抖擻精神,不要搞得這麼傷感。
Espresso,Cappuccino,Latte……是不是傻傻分不清?如果你只喝美式咖啡(和現在的我一樣),或者只喝速溶咖啡(和過去的我一樣),或者有選擇恐懼症,那麼從喝咖啡的角度來說,接下來的內容可能跟你關係不大。但咱們不是來學習設計模式的嘛。
3.2. 新的需求:一名咖啡師的自我修養
作為喝咖啡的人,可以完全不去探究喝到嘴裡的究竟是什麼,只要味道好就可以了。但如果你要成為一名咖啡師,那可就要好好修煉了。我們先來看看常見的幾種咖啡是怎麼做出來的。
好像也沒那麼複雜,不同的咖啡就是成分不同而已。那麼我們開始動手吧。
3.3. 解決方案
做一杯Coffee就是例項化一個Coffee物件,那麼就需要一個Coffee類,它包含至少兩個成員:Cost用來計算費用,Description用來描述成分。
3.3.1. 非常蠢的做法:Kaboom!
3.2.中列舉了9種Coffee,那就建立9個ConcreteCoffee類吧。每新增加一種Coffee,就得新增加一個ConcreteCoffee類。甚至原有的Coffee需要一些成分比例的調整時,也得新增加一個ConcreteCoffee類。這樣下去,類會多得爆炸的,Kaboom!
這違背了“針對介面程式設計,不針對實現程式設計”的OO原則。
再者,不同種類Coffee的組成也不是完全不同的,比如Espresso就出現在每一種Coffee中,它的Description和Cost屬性完全應該被其他種類的Coffee複用。
這違背了“封裝變化”的OO原則。
這樣不僅很沒有原則,而且真的是蠢哭了。
3.3.2. 一般蠢的做法:擴充超類的成員
為了增加複用性,一個方法就是擴充超類成員,把3.2.中每種咖啡成分的描述及費用都包含進來,子類(ConcreteCoffee類)繼承超類之後根據實際情況設定屬性即可。以Latte類為例(這裡省略了對成分的描述)。
Coffee基類
public class Coffee { // 每種成分的費用 private const float espressoCost = 5; private const float milkFoamCost = 1; private const float steamedMilkCost = 3; // 是否需要某種成分 public bool HasEspresso { get; set; } public bool HasMilkFoam { get; set; } public bool HasSteamedMilk { get; set; } // Constructor,初始狀態時不包含任何成分 public Coffee() { HasEspresso = false; HasMilkFoam = false; HasSteamedMilk = false; } // 根據成分計算總費用 public float Cost() { float totalCost = 0; if (HasEspresso) { totalCost += espressoCost; } if (HasMilkFoam) { totalCost += milkFoamCost; } if (HasSteamedMilk) { totalCost += steamedMilkCost; } return totalCost; } }
ConcreteCoffee類
public class Latte : Coffee
{
public Latte()
{
this.HasEspresso = true;
this.HasMilkFoam = true;
this.HasSteamedMilk = true;
}
}
來做一杯Latte吧:
Coffee coffee = new Latte();
Console.WriteLine(coffee.Cost());
3.4. 新的需求:更多的成分,更多的組合
3.3.2.中解決了3.3.1.中的問題,但這並不意味著就沒有問題了,一名有理想的咖啡師總是要考慮得更多一點。
1. 大部分Coffee並不需要那麼多成分,所以ConcreteCoffee類並不需要從Coffee基類中繼承所有的成員。你還記得1.3.1.中那個鴨子滿天飛的世界嗎?這裡也違背了“多用組合,少用繼承”的OO原則。
2. Coffee成分的種類、費用、比例都有可能發生變化(新的Coffee調配方法,原料價格變動,不同顧客的口味偏好),按照現有的結構,Coffee基類需要一直被修改。變化的部分存在於超類中,這違背了“封裝變化”的OO原則。
3.5. 解決方案:多用組合,少用繼承
如何改進呢?別忘了,還有一個“多用組合,少用繼承”的OO原則。
3.5.1. 成分
不同ConcreteCoffee物件的各種成分有著一些共同的特徵,比如費用,比如對成分的描述。我們可以建立一個AbstractComponent類,每種具體成分都作為一個ConcreteComponent類去繼承它。
AbstractComponent類
public abstract class CoffeeComponent
{
public float Cost { get; set; }
public string Description { get; set; }
public float Quantity { get; set; }
public void Add(CoffeeComponent coffeeComponent)
{
this.Cost += coffeeComponent.Cost;
this.Description = string.Format("{0} + {1}", this.Description, coffeeComponent.Description);
}
public void GetDescription()
{
Console.WriteLine("Total cost: " + this.Cost);
Console.WriteLine("This coffee consists of: " + this.Description);
Console.WriteLine();
}
}
ConcreteComponent類
public class Espresso : CoffeeComponent
{
private const float cost = 5;
public Espresso(float quantity)
{
this.Quantity = quantity;
this.Cost = cost * this.Quantity;
this.Description = this.Quantity + " Espresso";
}
}
3.5.2. 由成分組成的咖啡
對於標準化的Coffee,可以建立由ConcreteComponent物件組成的ConcreteCoffee類。每個ConcreteCoffee物件都是由若干ConcreteComponent物件組成的,每個ConcreteComponent物件就像是一個裝飾者(Decorator)一樣去裝飾其他的ConcreteComponent物件,它本身也可以被其他的ConcreteComponent物件所裝飾。
建立一個繼承自CoffeeComponent類的AbstractCoffee類,該類具有一個CoffeeComponent物件成員,該成員由若干CoffeeComponent物件組成。所有的ConcreteCoffee類都繼承自AbstractCoffee類。
AbstractCoffee類:
public abstract class AbstractCoffee : CoffeeComponent
{
public CoffeeComponent Coffee { get; set; }
}
ConcreteCoffee類:
public class Cappuccino : AbstractCoffee
{
public Cappuccino()
{
this.Description = "Cappuccino";
this.Coffee = new Espresso((float)1);
this.Coffee.Add(new SteamedMilk((float)0.5));
this.Coffee.Add(new MilkFoam((float)1));
}
}
對於定製化的Coffee,既可以單獨建立一個 ConcreteCoffee類,也可以在執行時決定新增哪些ConcreteComponent物件。
與3.3.中的方法對比:
以前的做法 |
現在的做法 |
先獲取所有的成分,再看需要什麼 |
需要哪種成分就單獨獲取這種成分 |
利用繼承,子類的行為是在編譯時靜態決定的 |
利用組合,子類的行為可以在執行時動態地擴充套件 |
3.6. 最終方案
3.6.1. UML
其中,Espresso,MilkFoam,SteamedMilk為ConcreteComponent類,繼承自CoffeeComponent類。Cappuccino,Latte為ConcreteCoffee類,繼承自AbstractCoffee類。
3.6.2. 測試程式碼
class Program
{
static void Main(string[] args)
{
// 標準化Coffee
Console.WriteLine("標準化Coffee:");
// Cappuccino
Coffee standardCoffee = new Cappuccino();
Console.WriteLine(string.Format("Make a cup of {0}.", standardCoffee.Description));
standardCoffee = ((Cappuccino)standardCoffee).Coffee;
standardCoffee.GetDescription();
// 執行時新增成分
Console.WriteLine("執行時新增成分:");
Console.WriteLine("Add more steamed milk.");
standardCoffee.Add(new SteamedMilk((float)5));
standardCoffee.GetDescription();
// 定製化Coffee
Console.WriteLine("定製化Coffee:");
Console.WriteLine("Make a cup of cutomized coffee.");
Coffee customizedCoffee = new Espresso((float)1);
customizedCoffee.Add(new MilkFoam((float)0.5));
customizedCoffee.Add(new SteamedMilk((float)0.2));
customizedCoffee.GetDescription();
}
}
3.6.3. 執行結果
標準化Coffee:
Make a cup of Cappuccino.
Total cost: 8.5
This coffee consists of: 1 Espresso + 0.5 Steamed Milk + 1 Milk Foam
執行時新增成分:
Add more steamed milk.
Total cost: 23.5
This coffee consists of: 1 Espresso + 0.5 Steamed Milk + 1 Milk Foam + 5 Steamed Milk
定製化Coffee:
Make a cup of cutomized coffee.
Total cost: 6.6
This coffee consists of: 1 Espresso + 0.5 Milk Foam + 0.2 Steamed Milk
3.7. 總結
【裝飾者模式】動態地將責任附加到物件上。若要擴充套件功能,裝飾者提供了比繼承更有彈性的替代方案。
包含的OO原則:
類應該對擴充套件開放,對修改關閉。對修改關閉指的是不修改現有的程式碼,尤其是不修改超類的程式碼。
(注:本文為筆者學習《Head First Design Patterns》的筆記,若涉及版權等問題,請告知。)