Strategy
January 3, 2022你想寫個 SortedIntegers
,加入 SortedIntegers
的物件會自動排序:
class SortedIntegers {
private List<Integer> integers = new ArrayList<>();
void add(Integer integer) {
integers.add(integer);
// 只是插入排序
for(var i = 0; i < integers.size(); i++) {
var to = insertTo(i);
if(to != -1) {
integers.add(to, integers.remove(i));
}
}
}
private int insertTo(Integer elemIdx) {
var elem = integers.get(elemIdx);
for(var i = 0; i < elemIdx; i++) {
if(integers.get(i) > elem) {
return i;
}
}
return -1;
}
List<Integer> integers() {
return integers;
}
}
public class Main {
public static void main(String[] args) {
var integers = Arrays.asList(10, 9, 1, 2, 5, 3, 8, 7);
var sorted = new SortedIntegers();
for(var integer : integers) {
sorted.add(integer);
}
// [1, 2, 3, 5, 7, 8, 9, 10]
out.println(sorted.integers());
}
}
排序的策略
嗯?升冪排列?不能降冪嗎?是可以另外寫一個方法來處理降冪啦!
...
void addDescending(Integer integer) {
integers.add(integer);
for(var i = 0; i < integers.size(); i++) {
var to = insertToDescending(i);
if(to != -1) {
integers.add(to, integers.remove(i));
}
}
}
private int insertToDescending(Integer elemIdx) {
var elem = integers.get(elemIdx);
for(var i = 0; i < elemIdx; i++) {
if(integers.get(i) < elem) {
return i;
}
}
return -1;
}
...
只不過可以發現程式碼幾乎與 add
、insertTo
重複,真正不同的地方只有在 if(integers.get(i) < elem)
,這部份決定了要升冪還是降冪,其他部份像是樣版般不變,不如讓使用者可以自行指定比較器?
package cc.openhome;
import java.util.*;
interface IntegerComparator {
int compare(Integer i, Integer j);
}
class SortedIntegers {
private List<Integer> integers = new ArrayList<>();
private IntegerComparator comparator;
SortedIntegers(IntegerComparator comparator) {
this.comparator = comparator;
}
...
private int insertTo(Integer elemIdx) {
var elem = integers.get(elemIdx);
for(var i = 0; i < elemIdx; i++) {
// 透過 comparator 比較
if(comparator.compare(integers.get(i), elem) > 0) {
return i;
}
}
return -1;
}
List<Integer> integers() {
return integers;
}
}
public class Main {
public static void main(String[] args) {
var integers = Arrays.asList(10, 9, 1, 2, 5, 3, 8, 7);
// 指定 Comparator 作為排序策略
var sorted = new SortedIntegers((i, j) -> i - j);
for(var integer : integers) {
sorted.add(integer);
}
System.out.println(sorted.integers());
}
}
這麼一來,使用者就可以自行指定升冪或降冪排列,當然,如果你熟悉 Java 的泛型,還可以進一步重構為支援泛型,不過這是另一番話了;其實 Java 中就有不少例子,例如 TreeSet
建構時,就可以指定 Comparator
,也支援泛型。
可抽換的演算策略
在 Gof 中,稱以上的實現概念為 Strategy 模式,如果你只看實現的程式碼,大概會搞不太清楚,這跟其他實現後有類似模樣的模式有什麼差別?
過於重視實現後的模樣,是許多人看待模式容易犯的錯誤!
模式不是一開始就存在,然後一群人照著模式的樣子來寫程式,而是一群人觀察程式碼後,從任務的聚焦、去除物件的相依性、抽取演算流程的樣版等各方面,對程式碼進行重構,然後發現重構的結果,似乎都有種相似性,為了便於溝通、傳承經驗,才取了個模式名稱。
Strategy 模式的名稱,代表該模式的成形過程,來自於觀察到演算流程中,可區分不變的演算樣版,以及會變動的特定演算。
至於為什麼會觀察到這個?因為需求!你可能一開始設計了個什麼,後來面對新的需求,為需求增加新的程式碼後,才發現到有可重用的演算樣版,於是才進行重構,有了 Strategy 模式的樣子。
就我而言,理解模式最好的方式是,有個需求,試著去實現它,增加需求,看看既有程式碼進一步滿足需求時,會有什麼問題,接著重構它,看看重構後的程式碼在面對類似的新需求時,是否有足夠的彈性。
就我而言,理解模式最差的方式是,有個需求,直接找一個模式,實現出長得像該模式的程式碼過程中,同時試著滿足需求,然後說…看!這個模式適合實現這種需求!…這種理解方式與先射箭後畫靶沒兩樣!
有人可能會說,「於觀察到演算流程中,可區分不變的演算樣版,以及會變動的特定演算」的話,重構之後最後也可能形成〈Template Method〉啊?
是有可能!不過,如果你真的是用重構的方式,最後構成了 Template Method,重構前的既有程式碼,應該本來就有繼承關係,或者說,觀察重構前的既有程式碼後,你還是決定真正的行為實現推給子類別實作(或許你就是想實現框架、hook 或 plugin 之類的需求),最後才會長得像是 Template Method 模式吧!
因為「於觀察到演算流程中,可區分不變的演算樣版,以及會變動的特定演算」,也有人稱 Strategy 模式為 Template callback 模式就是了!