在四人幫 (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
}
}
不但簡潔有力,而且還能享有自由抽換演算法的能力,就算是換了一個帳戶,程式碼也幾乎不用改。
不過聰明如你,可能也注意到了,演算法的實作其實用多型 (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,只要應用得當,能讓應用程式獲取更寬廣的空間,何樂而不為呢?
References:
https://en.wikipedia.org/wiki/Sorting_algorithm
https://en.wikipedia.org/wiki/Strategy_pattern