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;
	}
    ...

只不過可以發現程式碼幾乎與 addinsertTo 重複,真正不同的地方只有在 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,重構前的既有程式碼,應該本來就有繼承關係,或者說,觀察重構前的既有程式碼後,你還是決定真正的行為實現推給子類別實作,最後才會長得像是 Template Method 模式吧!