策略模式
策略模式的用意是針對一組演算法,將每一個演算法封裝到具有共同介面的獨立類中,從而使得它們可以相互替換。策略模式使得演算法可以在不影響到客戶端的情況下發生變化。
策略模式的結構
策略模式是對演算法的包裝,是把使用演算法的責任和演算法本身分開。策略模式通常是把一系列的演算法包裝到一系列的策略類裡面,作為一個抽象策略類的子類。
策略模式涉及到三個角色:
1、環境角色
持有一個策略Strategy的引用
2、抽象策略角色
這是一個抽象角色,通常由一個介面或抽象類實現,此角色給出所有具體策略類所需的介面
3、具體策略角色
包裝了相關演算法或行為
策略模式示例
有一個抽象的策略介面:
public interface Strategy { public void useStrategy(); }
實現兩種具體的策略:
public class StrategyA implements Strategy { public void useStrategy() { System.out.println("StrategyA.useStrategy()"); } }
public class StrategyB implements Strategy { public void useStrategy() { System.out.println("StrategyB.useStrategy()"); } }
某個類持有策略的引用:
public class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public void strategyMethod() { strategy.useStrategy(); } }
呼叫這個類的地方可以自行決定使用哪種策略:
public class TestMain { public static void main(String[] args) { Strategy strategyA = new StrategyA(); Strategy strategyB = new StrategyB(); Context context = new Context(strategyA); context.strategyMethod(); context = new Context(strategyB); context.strategyMethod(); } }
策略模式的使用場景
1、購物系統
舉一個實際例子吧。假如有一個購物系統,在使用者付款的時候,會產生很多場景,根據使用者的不同情況算出不同使用者要付款的金額,這時候最直觀的一種做法是:
在付款的裡面寫N多的if...else if...else,判斷使用者的場景,根據場景計算使用者付款金額。
這種設計明顯違反了開閉原則。開閉原則的"閉",指的是對修改關閉,但是這裡假如演算法又多了幾種,那麼必須再次修改這個付款的類。
這時候就可以使用策略模式。在付款的類裡面持有一個付款介面的引用,每次根據不同場景傳入一個具體的策略就好了。比如A類中要使用S0演算法,就傳入一個S0策略;B類中要使用S1演算法,就傳入一個S1演算法。不需要把判斷都放在付款的類中,程式碼的可讀性、可維護性也更高了。付款這個類甚至可以直接生成一個.class檔案放在一個jar包裡面供呼叫。
2、使得程式碼更優雅、更易維護
假如你的程式碼中某處有一個打分系統,你為這個打分系統寫了一段非常長的邏輯,某天,產品部門的同事找你,給我換一段打分邏輯,此時,有兩種做法:
(1)把原有的打分邏輯刪除,但這麼做一個缺點是是看不到以前的打分演算法了,另一個缺點是如果以後打分演算法要換回來就找不到程式碼,雖然SVN和GIT這種版本管理工具都有歷史提交記錄的功能,但還是顯得麻煩
(2)把原有的打分邏輯註釋,但這麼做的最大缺點是程式碼中有大量的註釋,尤其在策略邏輯非常長的時候,這就導致了程式碼的可讀性非常差
此時,就可以使用策略模式,將打分邏輯抽象為一種策略,換打分策略,新增一個策略的實現類,最後再讓程式碼中傳入新的策略實現類即可。
策略模式在Java中的應用及解讀
策略模式在Java中的應用,這個太明顯了,因為Comparator這個介面簡直就是為策略模式而生的。Comparable和Comparator的區別一文中,詳細講了Comparator的使用。比方說Collections裡面有一個sort方法,因為集合裡面的元素有可能是複合物件,複合物件並不像基本資料型別,可以根據大小排序,複合物件怎麼排序呢?基於這個問題考慮,Java要求如果定義的複合物件要有排序的功能,就自行實現Comparable介面或Comparator介面,看一下sort帶Comparator的過載方法:
1 public static <T> void sort(List<T> list, Comparator<? super T> c) { 2 Object[] a = list.toArray(); 3 Arrays.sort(a, (Comparator)c); 4 ListIterator i = list.listIterator(); 5 for (int j=0; j<a.length; j++) { 6 i.next(); 7 i.set(a[j]); 8 } 9 }
跟一下第3行:
1 public static <T> void sort(T[] a, Comparator<? super T> c) { 2 T[] aux = (T[])a.clone(); 3 if (c==null) 4 mergeSort(aux, a, 0, a.length, 0); 5 else 6 mergeSort(aux, a, 0, a.length, 0, c); 7 }
傳入的c不為null,跟一下第6行的mergeSort:
1 private static void mergeSort(Object[] src, 2 Object[] dest, 3 int low, int high, int off, 4 Comparator c) { 5 int length = high - low; 6 7 // Insertion sort on smallest arrays 8 if (length < INSERTIONSORT_THRESHOLD) { 9 for (int i=low; i<high; i++) 10 for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--) 11 swap(dest, j, j-1); 12 return; 13 } 14 15 // Recursively sort halves of dest into src 16 int destLow = low; 17 int destHigh = high; 18 low += off; 19 high += off; 20 int mid = (low + high) >>> 1; 21 mergeSort(dest, src, low, mid, -off, c); 22 mergeSort(dest, src, mid, high, -off, c); 23 24 // If list is already sorted, just copy from src to dest. This is an 25 // optimization that results in faster sorts for nearly ordered lists. 26 if (c.compare(src[mid-1], src[mid]) <= 0) { 27 System.arraycopy(src, low, dest, destLow, length); 28 return; 29 } 30 31 // Merge sorted halves (now in src) into dest 32 for(int i = destLow, p = low, q = mid; i < destHigh; i++) { 33 if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0) 34 dest[i] = src[p++]; 35 else 36 dest[i] = src[q++]; 37 } 38 }
第10行,根據Comparator介面實現類的compare方法的返回結果決定是否要swap(交換)。
這就是策略模式,我們可以給Collections的sort方法傳入不同的Comparator的實現類作為不同的比較策略。不同的比較策略,對同一個集合,可能會產生不同的排序結果。
認識策略模式
應當明白,策略模式的重心不是如何實現演算法(就如同工廠模式的重心不是工廠中如何產生具體子類一樣),而是如何組織、呼叫這些演算法,從而讓程式結構更靈活,具有更好的維護性和擴充套件性。
策略模式有一個很大的特點就是各策略演算法的平等性。對於一系列具體的策略演算法,大家的地位是完全一樣的,正因為這個平等性,各個演算法之間才可以相互替換。
執行期間,每一個時刻只能使用一個具體的策略實現物件,雖然可以動態地在不同的策略中實現切換。
策略模式的優缺點
優點
1、避免了多重條件if...else if...else語句,多重條件語句並不容易維護
2、策略模式提供了管理相關演算法簇的辦法,恰當使用繼承可以把公共程式碼移到父類,從而避免了程式碼重複
缺點
1、客戶端必須知道所有的策略類,並自行決定使用 哪一個策略,這意味著客戶端必須理解這些演算法的區別,以便選擇恰當的演算法
2、如果備選策略很多,物件的資料會很多