1. 程式人生 > >3. 裝飾者模式:裝飾物件

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》的筆記,若涉及版權等問題,請告知。)