繼承 vs. 委派#

1990 年代初期,OO 社群對繼承(inheritance)非常著迷——透過繼承可以「因差異而編程」(program by difference),只需建立子類別就能重用程式碼。然而到了 1995 年,GoF 提出了著名的建議:「優先使用物件組合而非類別繼承」(Favor object composition over class inheritance)。

本章探討的兩個模式正好體現了繼承與委派(delegation)的差異:

  • Template Method 使用繼承來解決問題
  • Strategy 使用委派來解決問題
  • 兩者都在解決同一個問題:將通用演算法與詳細實作分離,符合 DIP(依賴反轉原則)

Template Method 模式#

基本結構#

幾乎所有程式都有這個基本的 main loop 結構:

Initialize();
while (!Done()) // main loop
{
  Idle();       // do something useful.
}
Cleanup();

Template Method 模式將這個通用結構擷取到抽象基底類別的已實作方法中,將所有細節延遲到抽象方法:

public abstract class Application
{
  private bool isDone = false;

  protected abstract void Init();
  protected abstract void Idle();
  protected abstract void Cleanup();

  public void Run()
  {
    Init();
    while (!Done())
      Idle();
    Cleanup();
  }
}

子類別只需繼承 Application 並填入抽象方法的實作即可。

Pattern Abuse(模式濫用)#

注意: 作者承認用 Template Method 來封裝 ftoc 這種簡單程式的 main loop 是模式濫用的好例子。設計模式的存在不代表它們應該被到處使用——在這個案例中,Template Method 的成本高於它帶來的效益。

Bubble Sort 範例#

更實用的 Template Method 範例是 Bubble Sort。將排序演算法抽取到抽象基底類別,把比較(OutOfOrder)和交換(Swap)延遲為抽象方法:

public abstract class BubbleSorter
{
  protected int DoSort()
  {
    // ... 排序演算法 ...
    for (...)
      for (...)
      {
        if (OutOfOrder(index))
          Swap(index);
      }
  }

  protected abstract void Swap(int index);
  protected abstract bool OutOfOrder(int index);
}

衍生類別如 IntBubbleSorterDoubleBubbleSorter 分別處理不同型別的陣列:

Figure 22.1: Bubble sorter structure

重點: Template Method 展示了物件導向程式設計中經典的重用形式——通用演算法放在基底類別,被繼承到不同的詳細情境中。但繼承是非常強的關係,衍生類別與基底類別密不可分。例如 IntBubbleSorterOutOfOrderSwap 方法完全可以用於其他排序演算法,但因為繼承了 BubbleSorter,它們被永遠綁在一起。

Strategy 模式#

基本結構#

Strategy 模式用完全不同的方式反轉通用演算法與詳細實作的依賴關係。不是把通用演算法放在抽象基底類別,而是放在一個具體類別中;抽象方法被定義在一個介面中,具體類別透過委派呼叫介面:

Figure 22.2: STRATEGY structure of the Application algorithm

public class ApplicationRunner
{
  private Application itsApplication = null;

  public ApplicationRunner(Application app)
  {
    itsApplication = app;
  }

  public void run()
  {
    itsApplication.Init();
    while (!itsApplication.Done())
      itsApplication.Idle();
    itsApplication.Cleanup();
  }
}

public interface Application
{
  void Init();
  void Idle();
  void Cleanup();
  bool Done();
}

Bubble Sort 的 Strategy 版本#

在 Strategy 版本中,BubbleSorter 變成具體類別,透過 SortHandler 介面委派比較和交換操作:

public interface SortHandler
{
  void Swap(int index);
  bool OutOfOrder(int index);
  int Length();
  void SetArray(object array);
}

IntSortHandler 實作 SortHandler 介面,但完全不知道 BubbleSorter 的存在。這意味著 IntSortHandler 可以被用於任何其他排序演算法(如 QuickBubbleSorter),而不僅僅是 BubbleSorter

技巧: Template Method 部分違反了 DIP——SwapOutOfOrder 的實作直接依賴排序演算法。Strategy 則沒有這種依賴。因此 Strategy 額外提供了一個好處:每個詳細實作可以被多個不同的通用演算法操縱

Template Method vs. Strategy 比較#

flowchart LR
    subgraph tm ["Template Method"]
        A1["抽象基底類別<br/>通用演算法"] -->|繼承| B1["子類別 A<br/>具體實作"]
        A1 -->|繼承| C1["子類別 B<br/>具體實作"]
    end
    subgraph st ["Strategy"]
        A2["具體類別<br/>通用演算法"] -->|委派| I2[介面]
        I2 -->|實作| B2[策略 A]
        I2 -->|實作| C2[策略 B]
    end
面向Template MethodStrategy
機制繼承委派(組合)
演算法位置抽象基底類別的已實作方法具體類別
詳細實作子類別覆寫抽象方法實作介面的獨立類別
耦合度衍生類別與基底類別緊密耦合實作類別與演算法類別完全解耦
DIP 符合度部分違反(衍生類別依賴基底類別)完全符合
靈活度較低(實作綁定到特定演算法)較高(實作可被多個演算法重用)
複雜度較低(少一個類別)較高(多一個類別和委派間接層)

結論#

  • Template Method 寫起來簡單、用起來簡單,但缺乏彈性
  • Strategy 具有彈性,但需要多建立一個類別、多一個物件、多一層佈線
  • 選擇取決於是否需要 Strategy 的靈活性,或者能接受 Template Method 的簡單性
  • 作者表示自己多數時候選擇 Template Method,因為它更容易實作和使用。除非確定需要不同的排序演算法,否則不必使用 Strategy