重溫《深入淺出設計模式》裝飾者模式 (Book Review of Head First Design Pattern, Decorator Pattern)

2020-06-29

以前第一次看到 Python 的 Decorator 時滿滿的黑人問號,後來有機會接觸裝飾者模式才豁然開朗,但也沒有深究,甚至沒有真的用過 Decorator。但最近重溫才發現其實裝飾者模式和 Python 的 Decorator 並不是完全的等號。Python 的 Decorator 更像是一種語法糖,利用 Python 函式 first class 的特性,能夠有更方便包裝函式的語法,可以為函式前後加入邏輯並且重利用程式碼。而要利用 Python 的 Decorator 也可以實踐 Decorator Pattern ,只是兩者並不是相等的,一個是語法模式,但背後有 Pattern 的精神;另一個則是 Pattern。

logo

裝飾者模式

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

裝飾者模式提供了一個有別於繼承的設計方式,讓程式碼不僅得以重複使用且易於擴充。本模式是以咖啡為範例,咖啡作為一種類別,而調味則是其需求,如果要因應各式需求,必須衍生繁瑣的類別。

裝飾者模式的 UML

Before Pattern

目前的設計會有以下問題:

  1. 如果有悉的配料,就要加入新的方法,並且變更 Beverage 類別中 cost 的邏輯
  2. 如果需求條件(配料)有特殊需求,例如雙倍、新類型,Beverage 類別就必須配合修改。
public class Beverage
{
  private float condimentCost = 0.0;
  public bool hasMilk()
  {
    ...
  }

  public bool hasMocha()
  {
    ...
  }

  public double cost()
  {
    if (hasMilk())
      condimentCost += milkCost;
    if (hasMocha())
      condimentCost += mochaCost;
    return condimentCost;
  }
}

public class DarkRoast : Beverage
{
  public double cost()
  {
    return 1.99 + super.cost();
  }
}

After Pattern

而裝飾者模式要求先設計一個類別與裝飾類別之間共通的類別(超類別),並且可以用一個或多個裝飾者包裝物件,最重要的是裝飾者可以在所委派被裝飾者的行為之前或之後,加上自己的行為達到特定目的,裝飾者提供了動態將責任加諸於物件的彈性,相較於繼承更易於擴充。

要實作裝飾者,最重要的就是

public abstract class Beverage
{
  private string _description = "unknown beverage";

  public string GetDescription()
  {
    return _description;
  }

  public abstract void Cost();
}

public abstract class CondimentDecorator : Beverage
{
  public abstract string GetDescription();
}

public class Espresso : Beverage
{
  public Espresso()
  {
    _description = "Espresso";
  }

  public double cost()
  {
    return 1.99;
  }
}

// Decorator 的靈魂,實作與被裝飾者相同的超類別,並且將被裝飾者當成自己的 private 成員,於是可以一層一層的呼叫
public class Mocha : CondimentDecorator
{
  private Beverage _beverage;

  public Mocha (Beverage beverage)
  {
    _beverage = beverage;
  }

  public string GetDescription()
  {
    return _beberage.GetDescription() + ", Mocha";
  }

  public double Cost()
  {
    return 0.2 + _beverage.Cost();
  }
}

Beverage beverage = new Espresso();
beverage = new Mocha(beverage);
Console.Writeline(beverage.GetDescription()); // Espresso, Mocha

小結

✔️將變動的部分封裝起來

  • 裝飾的類別會衍生,所以個別封裝

✔️多用合成,少用繼承

  • 根據裝飾者模式的精神,演算邏輯由合成的類別實作

✔️類別應該對需求保持開放;應該對既有的程式碼修改保持關閉

  • 實作新的裝飾類別或者被裝飾類別,並不會互相影響