[.NET][Design Patterns] 容易被誤會的 Strategy Pattern

在四人幫 (GoF) 的設計模式 (Design Patterns) 一書中,最容易被用到的模式,非策略模式 (Strategy Pattern) 莫屬了,不過也就是因為它太容易被用到,它也是很容易被誤會的一種設計模式,特別是和多型 (Polymorphism) 的混淆。

策略模式的設計,是為了要能實現在同一個問題領域 (Problem Domain) 內,能依照環境 (Context) 的不同,在不改變原始程式碼的情況下,自由的抽換適用於環境或是由環境指定的解決方法 (Solution),這個抽換基本上不能影響叫用它的程式碼 (也就是用戶端),而更近一步的做到動態抽換。

策略 (Strategy) 在不同的領域有著不同的定義,可以大到戰略等級的定義,也可以小到指導做一件事的方法,不過我個人覺得賽局理論內的策略的定義比較適合:

在賽局理論裡,玩家在賽局中的策略是指在所有可能發生情況下的一套完整行動計畫;這完全決定了玩家的行為。
玩家的策略會決定玩家在賽局的任一階段所採取的行動,不論這一階段之前是如何演變而來的

簡單的說,策略其實在日常生活中經常發生,舉凡:

  • 每天上班要怎麼走才會最快到公司或回家。
  • 每天移動要使用的交通工具的選擇。
  • 每天中午在 100 元內要吃飽的吃法。
  • 存錢要存哪裡才會有高利息。
  • 貸款要向哪家銀行貸才能省錢。
  • 在晚餐時要做什麼才能讓女生好感度增加。

而在寫程式或設計系統時,總會有這樣的流程存在,輸入和輸出可能是固定的,但中間的流程會依照不同的需求而有改變,以致於輸入的值會因為中間的流程而讓輸出的值改變,例如銀行存款,普通帳戶可能只有 0.5% 利息,但如果是 VIP 帳戶可能會有 1.5% 利息,而計算利息的方法是固定的:本金 x (1 + 利息%) ,而利息可以用切換的方式來得到不同的結果,因此程式就可以這樣寫:

// abstract
public interface ISavingRate
{
    double Calculate(double principalAmount);
}

// implementation
public class NormalSavingRate : ISavingRate
{
    private readonly _rate = 0.005;

    public double Calculate(double principalAmount)
    {
        return principalAmount * (1.0 + _rate);
    }
}


public class VipSavingRate : ISavingRate
{
    private readonly _rate = 0.015;

    public double Calculate(double principalAmount)
    {
        return principalAmount * (1.0 + _rate);
    }
}



然後再利用相依於抽象的方式,叫用它就可以了。

public double GetSavingAmount()
{
    ISavingRate savingRate = GetSavingRate(_account.SavingAccountType);
    return savingRate.Calculate(_account.PrincipalAmount);
}

private ISavingRate GetSavingRate(SavingAccountType type)
{
    switch (type)
    {
        VIP:
           return new VipSavingRate();
        Normal:
           return new NormalSavingRate();
        default:
           return new NullSavingRate(); // Null Object Pattern
    }
}

不但簡潔有力,而且還能享有自由抽換演算法的能力,就算是換了一個帳戶,程式碼也幾乎不用改。

這裡講幾乎,是因為 switch 的關係,但如果結合 Chain of Responsibility 的話,也可以消除 switch,完全做到不用改程式的目標。

不過聰明如你,可能也注意到了,演算法的實作其實用多型 (Polymorphism) 就可以解決了,用抽象類別覆寫也可以解決,所以就會看到有些文章會拿多型當策略樣式的說法。

其實這是不對的。

多型是指在同一個物件的宣告 (或佈局) 之下,依不同的實作而有不同的結果或反應,當然貓狗和動物的例子太多,在這就不舉例了,光拿前面的存款利息的程式,它本身就是一個多型的例子,對於不熟悉策略模式的初學者而言,很容易和多型混淆在一起,事實上,設計模式很仰賴物件導向的多型性質,沒有多型的能力基本上很難實作出設計模式,但是不能因為設計模式是多型的一種,就反過來說多型就是設計模式,這反而是錯誤的。

那麼,所謂的策略模式是什麼?根據 GoF 的定義,策略模式是:

定義一整族的演算法,將每個演算法封裝起來,可互換使用,更可在不影響外界的情況下個別抽換所引用的演算法。

光是多型,是做不到在不影響外界的情況下個別抽換的能力,這還會需要其他的方法,例如工廠模式 (Factory Pattern),以及相依注入 (Dependency Injection) 等機制。

其實不用舉什麼貓狗的例子,其實只要是學資訊的,都能很快想到一個好例子:排序演算法 (Sorting Algorithm)。

排序演算法有這麼多種,光維基百科上列出來的就有幾十種,教科書上經常教的至少也有五種以上,什麼快速排序、合併排序、基數排序、氣泡排序、堆積排序等等,但其實它們都只有一個目的:排序,所以可以將要實作的排序演算法的規則定義成:

public interface ISortingAlgorithm<T>
{
    IEnumerable<T> Sort(IEnumerable<T> source);
}

將排序演算法依這個規則實作後,上層只需要這樣:

private IEnumerable<T> _source;

...

public IEnumerable<T> Sort()
{
    var algorithm = GetSortingAlgorithm<T>(); // implement Factory for generate algorithm.
    return algorithm.Sort(_source);
}

日後我想要抽換演算法,只要修改 GetSortingAlgorithm() 的內容,或直接以 Dependency Injection 技術實作 GetSortingAlgorithm(),那這樣只要修改組態檔就能解決動態抽換的問題了。

而且,有些策略不是簡單幾行程式就能解決的,策略模式也可以用來實作政策 (Policy) 的演算法,因此策略模式也可以被稱為政策模式 (Policy Pattern),也可以像我一開始說的,策略可大可小,因此策略模式的實作內也有可能包含其他的策略模式的實作,這些都要看設計者怎麼思考與實作。

設計模式的使用是需要思考,而非一股腦的實作,否則很容易到後面偏掉或做錯。

如同我在前一篇文章所提到的,策略模式也是一個很容易,幾乎是隨手就能實作的一種 Pattern,只要應用得當,能讓應用程式獲取更寬廣的空間,何樂而不為呢?

只有一個 Pattern 或許無法完全解決問題,但只要結合多種 Pattern,讓它能發揮綜效,問題會在不知不覺中就被解決了。

References:

https://en.wikipedia.org/wiki/Sorting_algorithm
https://en.wikipedia.org/wiki/Strategy_pattern