重溫《深入淺出設計模式》策略模式 (Book Review of Head First Design Pattern, Strategy Pattern)


  1. 策略模式
    1. Before Pattern
    2. After Pattern
  2. 小結

設計模式是很需要開發經驗來輔助學習的,一兩年前就曾經翻閱過這本書,只是當時對物件導向的體會仍是懵懂,而僅有物件導向的知識也無法自然的學會設計模式的使用,因為設計模式需要程式開發的經驗累積,在開發中必須實際使用物件導向,同時需要對話、討論,並且實際體驗過擴充、維護的痛楚,才能歸結出設計模式。而直接學習設計模式儼然就是快速增長物件導向的設計功力,但也因為缺少了實際感受到設計模式美好的過程,所以學習容易流於浮光掠影的記憶。

這次的學習除了閱讀本身,更強調實作,除了重新詮釋閱讀素材的案例並改寫成 C# Code外,也將模式前後的差別書寫成部落格,期待讓學習更深植腦中,能夠設計模式真正的成為自己的一部分。

logo

策略模式

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

在學習物件導向之後,心理有一種認定就是介面優於繼承,因此看到情境使用繼承該如何寫得更容易擴充、維護時,直覺就是改用介面來替代。然而卻忽略了繼承其實是具有程式碼重用的優點,如果改為介面,重複的程式碼反而會一再出現。

發現這點的時候其實挺震驚的,那到底該怎麼辦,才能兼具兩者的優點?而答案就是策略模式,這個模式同時也刷新了我對封裝的認知,能夠封裝的不只是具現的事物,抽象的行為(或者說是演算邏輯)也可以做為封裝。

範例是以實作各式鴨子為範例,一開始使用繼承很容易讓衍生類別多了不相干的方法;但使用介面又會出現重複程式碼的問題。

裝飾者模式的 UML

Before Pattern

public class Duck
{
  public void quack()
  {
    ...
  }
  public void swim()
  {
    ...
  }
  public void display()
  {
    ...
  }
}

public class MallardDuck : Duck
{
  public override void display()
  {
    ...
  }
}

public class RedHeadDuck : Duck
{
  public override void display()
  {
    ...
  }
}

After Pattern

一個可行的方案就是將會不斷改變的部分加以封裝,在這個案例中就是鴨子的行為 (Swim, Fly) 等,可能會因為需求產生多種的 Swim, Fly 實作。並使用合成取代繼承 (Composition over inheritance)。讓行為擁有一個共通介面,並依據擴充需求實作介面,而鴨子類別擁有行為共通介面,由執行時期去依據行為的邏輯不同去實作。

public abstract class Duck
{
  FlyBehavior flyBehavior;

  public void performFly()
  {
    flyBehavior.fly();
  }
}

public class FlyWithWings : FlyBehavior
{
  public void fly()
  {
    ...
  }
}

public class FlyNoWay : FlyBehavior
{
  public void fly()
  {
    ...
  }
}

public class MallardDuck : Duck
{
  public MallardDuck()
  {
    flyBehavior = new FlyWithWings();
  }
}

如此一來鴨子鴨子行為(演算邏輯)分為兩個類別,兩者不互相依賴,如果要擴充鴨子的行為也不會影響到鴨子類別。同時如果將鴨子類別,加入 FlyBehaviorSetter,甚至可以在執行期 (RunTime) 動態的調整鴨子行為。

public void setFlyBehavior(FlyBehavior fb)
{
  flyBehavior = fb;
}

var duck = new MallardDuck();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();

小結

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

  • 思考什麼是會變動的?
  • 思考什麼是可以封裝的?

✔️多用合成,少用繼承

  • 降低耦合
  • 根據策略模式的精神,演算邏輯由合成的類別實作

✔️針對介面撰寫,而非針對實踐方式撰寫。

  • 避免讓演算邏輯跟著衍生類別
  • 讓演算邏輯獨立成為物件,並且能夠經由合成的方式共用