引子:明天該穿什麼?#

小菜要去見嬌嬌,求大鳥指點該穿什麼。大鳥順勢出題:寫一個給人搭配服飾的系統,類似 QQ、網路遊戲或論壇的 Avatar 系統。

第一版:把所有裝扮都塞進 Person#

class Person
{
    public void WearTShirts() { ... }
    public void WearBigTrouser() { ... }
    public void WearSneakers() { ... }
    public void WearSuit() { ... }
    public void WearTie() { ... }
    public void WearLeatherShoes() { ... }
    public void Show() { ... }
}

要新增「超人」裝扮就得修改 Person 類別——這違反了開放-封閉原則

第二版:抽象服飾類#

把每件服飾都變成獨立子類:

abstract class Finery
{
    public abstract void Show();
}

class TShirts : Finery { public override void Show() => Console.Write("大 T 恤 "); }
class BigTrouser : Finery { public override void Show() => Console.Write("垮褲 "); }
// ...

但客戶端變成「跳脫衣舞」:

dtx.Show();
kk.Show();
pqx.Show();
xc.Show();

這種寫法等於「光著身子當著大家面,先穿 T 恤、再穿褲子、再穿鞋」——應該在內部組裝完成後再顯示。

這也不是建造者模式(Builder):建造者要求建造過程必須穩定,但服飾組合過程是不穩定的(先穿西裝再套 T 恤、加披風、打領帶都行)。

裝飾模式#

裝飾模式(Decorator Pattern):動態地給一個物件添加一些額外的職責;就增加功能來說,裝飾模式比生成子類更為靈活。[DP]

結構#

  • Component:定義物件介面,可以給這些物件動態地添加職責
  • ConcreteComponent:具體物件
  • Decorator:裝飾抽象類,繼承 Component,從外類擴展 Component 的功能;對 Component 而言不需要知道 Decorator 的存在
  • ConcreteDecorator:具體裝飾物件,給 Component 添加職責
classDiagram
    class Component {
        <<abstract>>
        +Operation()
    }
    class ConcreteComponent {
        +Operation()
    }
    class Decorator {
        <<abstract>>
        -Component component
        +SetComponent(Component)
        +Operation()
    }
    class ConcreteDecoratorA {
        +Operation()
    }
    class ConcreteDecoratorB {
        +Operation()
    }
    Component <|-- ConcreteComponent
    Component <|-- Decorator
    Decorator o--> Component
    Decorator <|-- ConcreteDecoratorA
    Decorator <|-- ConcreteDecoratorB
abstract class Component
{
    public abstract void Operation();
}

class ConcreteComponent : Component { ... }

abstract class Decorator : Component
{
    protected Component component;
    public void SetComponent(Component component) => this.component = component;
    public override void Operation()
    {
        if (component != null) component.Operation();
    }
}

class ConcreteDecoratorA : Decorator
{
    public override void Operation()
    {
        base.Operation();
        // 本類額外的功能
    }
}

客戶端:

ConcreteComponent c = new ConcreteComponent();
ConcreteDecoratorA d1 = new ConcreteDecoratorA();
ConcreteDecoratorB d2 = new ConcreteDecoratorB();
d1.SetComponent(c);
d2.SetComponent(d1);
d2.Operation();

裝飾物件透過 SetComponent 串接,使每個裝飾物件的實作與「如何使用」分離。

每個裝飾物件只關心自己的功能,不需要關心如何被加到物件鏈當中。

第三版:裝飾模式重構#

簡化建議:當只有一個 ConcreteComponent 時,可以省略 Component 抽象類,讓裝飾類直接繼承具體類別。

class Person // 即 ConcreteComponent
{
    private string name;
    public Person(string name) { this.name = name; }
    public Person() { }
    public virtual void Show() => Console.WriteLine($"裝扮的 {name}");
}

class Finery : Person // 即 Decorator
{
    protected Person component;
    public void Decorate(Person component) { this.component = component; }
    public override void Show()
    {
        if (component != null) component.Show();
    }
}

class TShirts : Finery
{
    public override void Show() { Console.Write("大 T 恤 "); base.Show(); }
}

class BigTrouser : Finery
{
    public override void Show() { Console.Write("垮褲 "); base.Show(); }
}

客戶端:

Person xc = new Person("小菜");
Sneakers pqx = new Sneakers();
BigTrouser kk = new BigTrouser();
TShirts dtx = new TShirts();
pqx.Decorate(xc);
kk.Decorate(pqx);
dtx.Decorate(kk);
dtx.Show();
// 輸出:大 T 恤 垮褲 破球鞋 裝扮的小菜

改變裝飾順序就改變顯示順序。不同的組合形成不同的形象——「光著膀子、打著領帶、下身垮褲、左腳皮鞋、右腳破球鞋」也能組出來。

何時使用裝飾模式#

  • 需要為已有功能動態地添加更多功能
  • 起初設計只在主類加新欄位、新方法、新邏輯時,會持續增加主類複雜度,這些新東西可能只在特定情況下執行
  • 裝飾模式把每個裝飾功能放到單獨的類別中,讓它包裝原物件,客戶端可在執行時按需要、按順序使用裝飾包裝物件

優點#

  • 把「裝飾功能」從主類別中搬出,簡化原有類別
  • 有效區分核心職責裝飾功能
  • 去除相關類別中重複的裝飾邏輯

注意事項#

裝飾模式的裝飾順序很重要

例如:加密資料與過濾詞彙都可以是資料持久化前的裝飾功能,但若先加密再過濾就會出問題。

最理想的情況,是保證裝飾類之間彼此獨立,這樣它們就能以任意順序進行組合。