設計模式筆記—Strategy Pattern

Strategy Pattern 與 Template Method Pattern 的相似,都是跟流程相關的模式
但有一些不一樣的地方

正如 Strategy Pattern 這個名子,這個模式著重在策略的部份
策略也是一種抽象的概念,可以說是達成某個目的過程中可能會用到方法
根據不同的場合使用不同的策略,靈活度高

Strategy Pattern 通常會透過一個介面來表示策略的抽象概念
接著只要把這個策略帶入任何需要使用的流程內即可

接下來會用常見的 Sorter 來做範例:

// 這邊簡單用個 selection sort :) 
public class SelectionSorter<T> {
// 比較的策略,會回傳數字, 1 表示 A > B,0 表示相等,-1 表示 B < A
private CompareStrategy<T> strategy;
public Sorter(CompareStrategy<T> strategy) {
this.strategy = strategy;
}

// 由小到大排序
public void Sort(T[] items) {
for (var i=0;i<items.Length;++i) {
for (var j=i+1;j<items.Length;++j) {
if (this.strategy.Compare(items[i], items[j]) == 1) {
var temp = items[i];
items[i] = items[j];
items[j] = temp;
}
}
}
}
}

在上面的程式中,一個叫 Sort 泛型類別會接受一個 CompareStrategy 的泛型介面
之後就可以用這個 Sorter 對元素進行 in-place 排序

有關 CompareStrategy 的定義如下:

public interface CompareStrategy<T> {
int Compare(T item1, T item2);
}

這個 CompareStrategy 就是一個策略,用於比較兩個數值的大小

現在要做一個複數的排序,直接用 .NET 內建的 Complex 來表示複數
先以複數的大小來排序,然後再依照實數、虛數的數值個別排序:

public class MyComplexCompareStrategy : CompareStrategy<Complex> {
public int Compare(Complex item1, Complex item2) {
if (item1.Magnitude != item2.Magnitude) {
return item1.Magnitude > item2.Magnitude ? 1 : -1;
} else if (item1.Real != item2.Real) {
return item1.Real > item2.Real ? 1 : -1;
} else if (item1.Imaginary != item2.Imaginary) {
return item1.Imaginary > item2.Imaginary ? 1 : -1;
} else { // 到這邊就表示完全相等
return 0;
}
}
}

然後建立一個複數的比較並進行排序:

var complexCompareStrategy = new MyComplexCompareStrategy();
var complexSorter = new SelectionSorter<Complex>(complexCompareStrategy);
var complexList = new Complex[] {
new Complex(1, 2),
new Complex(0, 0),
new Complex(2, 1),
new Complex(3, 4),
new Complex(-2, 1)
};

Console.WriteLine("Before: {0}", string.Join(", ", complexList));
// Before: <1; 2>, <0; 0>, <2; 1>, <3; 4>, <-2; 1>
complexSorter.Sort(complexList);
Console.WriteLine("Sorted: {0}", string.Join(", ", complexList));
// Sorted: <0; 0>, <-2; 1>, <1; 2>, <2; 1>, <3; 4>

結果如下:

image-20230319222209823

可以再更簡單一些

看過了上面的範例,應該可以感受到這個模式的威力
搭配泛型使用,整個類別變得很有彈性,適合不同的場合使用

原始的 Strategy Pattern 就是這種有介面與實做類別的形式,寫起來 Class 的數量很容易爆增
但由於現在的 OO 語言多少都會融合一點 FP 的概念
Lambda 的導入可以說避免產生太多的 Class
我們可以直接把上面的程式化簡化這樣:

public class SortAlgorithm {
// comparer 接受 2 個 T 參數,回傳 int
public void SelectionSort<T>(T[] items, Func<T, T, int> compare) {
for (var i=0;i<items.Length;++i) {
for (var j=i+1;j<items.Length;++j) {
if (compare(items[i], items[j]) == 1) {
var temp = items[i];
items[i] = items[j];
items[j] = temp;
}
}
}
}
}

執行的程式可以簡化成:

var comparer = (Complex item1, Complex item2) => {
if (item1.Magnitude != item2.Magnitude) {
return item1.Magnitude > item2.Magnitude ? 1 : -1;
} else if (item1.Real != item2.Real) {
return item1.Real > item2.Real ? 1 : -1;
} else if (item1.Imaginary != item2.Imaginary) {
return item1.Imaginary > item2.Imaginary ? 1 : -1;
} else { // 到這邊就表示完全相等
return 0;
}
}
var complexList = new Complex[] {
new Complex(1, 2),
new Complex(0, 0),
new Complex(2, 1),
new Complex(3, 4),
new Complex(-2, 1)
};

Console.WriteLine("Before: {0}", string.Join(", ", complexList));
// Before: <1; 2>, <0; 0>, <2; 1>, <3; 4>, <-2; 1>
SortAlgorithm.SelectionSort(complexList, comparer);
Console.WriteLine("Sorted: {0}", string.Join(", ", complexList));
// Sorted: <0; 0>, <-2; 1>, <1; 2>, <2; 1>, <3; 4>

除了策略本身透過可以 lambda 簡化並直接存到變數以外
流程本身也可以只是一個方法,然後接受一個函數來使用

這其實就跟在 C 裡面用函式指標是同一個意思
我會把這邊的 compare 函數變數作為是一種策略

做到這邊,最後就會發現,.NET 的 Array.Sort 跟範例很像:

public delegate int Comparison<in T>(T x, T y);
public static void Sort<T>(T[] array, Comparison<T> comparison);

思考模式的概念

從最開始有提過,Strategy Pattern 著重在策略的部份
策略就是這個模式的核心
將外部提供的策略注入到某個類別中使用

概念上就好比一個產品從生產到進入市場的流程
市場調查、研發、生產、最後打入市場
打入市場勢必需要某種策略,針對不同的客群使用不同的方式讓客戶對產品產生興趣

因為只關注策略而非流程,因此策略本身可以在各種流程中使用,提高了靈活度

與 Template Method Pattern 之間該如何做選擇

Template Method Pattern 與 Strategy Pattern 可以做同樣的事情,在靈活度跟關聯程度上也所差異
我覺得要從問題本身來思考:

現在要解決的問題是需要一套共通的作法,還是依場合套用不同策略

了解問題的需求,可以從兩個模式中做選擇了

例如說排序,通常都會是使用者自己按需求排序
它需要使用者選擇一個比較策略,所以就會使用 Strategy Pattern

但如果在我的應用程式中,排序跟它所屬的環境有關
換句話說,它跟環境之間有比較強烈的關聯性,其他人不會隨意去更動比較規則
那使用 Template Method Pattern 就會很適合

後記

最近太懶了,一直沒有更新設計模式
雖然我也沒辦法寫太快,畢竟我需要一些例子來說明模式
有些模式實在是不太常見,我自己也很難想像什麼時候會用到
可能是因為我經驗還不夠,還沒見過各種優秀 (或慘絕人寰?) 的程式

無暇的程式碼—敏捷完整篇 書中,Strategy Pattern 跟 Template Method Pattern 是一起講的
書中的作者因為 Template Method Pattern 比較簡單而選擇使用該模式
但我自己的想法是,要去思考問題背後的抽象概念,根據關注的東西來選擇模式的使用
不過問題的概念可能隨著需求而有所改變,最後還是有機會轉去另一個模式

目前我對於設計模式的理解都來自 無暇的程式碼—敏捷完整篇 這本書
但實際上我並沒有特別去看過原本的設計模式書籍,頂多上網看一點文章而已,並不了解模式的背景故事
如果有什麼錯誤的地方,或是覺得解釋不好的,還請多多包含 <( )>