引子:明天該穿什麼?#
小菜要去見嬌嬌,求大鳥指點該穿什麼。大鳥順勢出題:寫一個給人搭配服飾的系統,類似 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 <|-- ConcreteDecoratorBabstract 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 恤 垮褲 破球鞋 裝扮的小菜改變裝飾順序就改變顯示順序。不同的組合形成不同的形象——「光著膀子、打著領帶、下身垮褲、左腳皮鞋、右腳破球鞋」也能組出來。
何時使用裝飾模式#
- 需要為已有功能動態地添加更多功能
- 起初設計只在主類加新欄位、新方法、新邏輯時,會持續增加主類複雜度,這些新東西可能只在特定情況下執行
- 裝飾模式把每個裝飾功能放到單獨的類別中,讓它包裝原物件,客戶端可在執行時按需要、按順序使用裝飾包裝物件
優點#
- 把「裝飾功能」從主類別中搬出,簡化原有類別
- 有效區分核心職責與裝飾功能
- 去除相關類別中重複的裝飾邏輯
注意事項#
裝飾模式的裝飾順序很重要。
例如:加密資料與過濾詞彙都可以是資料持久化前的裝飾功能,但若先加密再過濾就會出問題。
最理想的情況,是保證裝飾類之間彼此獨立,這樣它們就能以任意順序進行組合。