【深入設計模式】策略模式—策略模式詳解及策略模式在原始碼中的應用
本文已參與「新人創作禮」活動,一起開啟掘金創作之路。
生活中我們經常會遇到選擇問題,比如當我們要出去旅遊時,會考慮是自駕、坐飛機還是坐火車前往目的地;或者在烹飪一條魚時,是考慮清蒸、水煮還是燒烤;又或者商家在對商品促銷時,是使用會員累計積分、打折促銷或者買贈的方式進行促銷。這個時候就需要根據當前不同的的條件,來選擇出對應的具體實現方式,這就是策略模式。在實際開發中,策略模式也是會經常使用的一種設計模式。在實現某個功能有多種方式可供選擇時,策略模式就能派上用場。
1. 策略模式
1.1 策略模式簡介
不知道在座的各位有沒有在維護專案程式碼時,看到過大段大段的 if else 語句,本人曾有幸遇到過一個方法裡面大量 if else 巢狀,並且每一個程式碼塊都很長。這種程式碼通常是第一版開發時判斷分支比較少,就是用 if else 來進行處理,隨著版本迭代,功能需求的增加,後面為了快速迭代就直接在原來的 if else 語句基礎上繼續新增判斷分支,久而久之就嵌套出了大量的判斷分支。這樣寫法雖然開發的人寫著快,但是對於後面程式碼維護或者新人閱讀程式碼是非常不友好,甚至感到崩潰的。那麼當我們在開發中發現判斷分支開始膨脹時,這個時候就可以考慮使用策略模式來進行處理。
策略模式定義了一系列功能的實現,而這些功能實現的目的是相同的,能夠使用相同的方式來呼叫所有的實現,只是呼叫時根據傳入不同引數從而獲取到不同的實現。使用這樣的方式將方法呼叫和功能實現進行分割,從而達到具體策略之間相互獨立,修改、新增策略實現時,不會對策略呼叫方和其他策略產生影響。
1.2 策略模式結構
在簡單瞭解了策略模式之後,我們來看看他的結構。
策略模式中需要定義一個策略介面 Strategy,使用具體策略類實現該介面來封裝具體的策略實現過程。同時還需要給呼叫方提供一個管理 Strategy 配置類 Context,呼叫方通過 Context 來呼叫具體的策略。
```java / * 策略介面 */ public interface Strategy { void strategyMethod(); } / * 具體策略 1 / public class SpecificStrategy1 implements Strategy { @Override public void strategyMethod() { } } / * 具體策略 2 / public class SpecificStrategy2 implements Strategy { @Override public void strategyMethod() {
}
}
/* * Strategy 配置管理類 context / public class Context { private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void strategyMethod() {
strategy.strategyMethod();
}
} ```
呼叫方程式碼
```java public static void main(String[] args) { // 呼叫具體策略 1 Context strategyContext1 = new Context(new SpecificStrategy1()); strategyContext1.strategyMethod();
// 呼叫具體策略 2
Context strategyContext2 = new Context(new SpecificStrategy2());
strategyContext2.strategyMethod();
} ```
我們可以看到在這樣的結構下,呼叫方只需要在構建 Context 的時候傳入具體的策略實現就可以了,呼叫方也不會在關心具體是怎麼實現的。如果要新增策略實現方式,則新增實現類即可,同樣修改實現也只需修改對應的實現類。
1.3 策略模式示例
前面對策略模式的概念和結構進行了介紹,可能還是會感覺有點雲裡霧裡,下面就用具體示例來加深理解。
場景模擬:假設你現在有一條魚準備烹飪,烹飪的方式有清蒸、烤魚兩種做法,那麼我們就可以使用策略模式來得到一條烹飪完成的魚。
首先我們定義一個烹飪策略類(CookStrategy)並定義 cookFish 方法,然後定義兩個具體的策略實現類 SteamedFish (水煮魚)和 GrillFish (烤魚)。根據結構定義我們還需要給呼叫方提供一個 CookContext 類來管理具體的策略和方法呼叫,程式碼如下:
```java // 烹飪策略類 public interface CookStrategy { void cookFish(); } // 清蒸魚 public class SteamedFish implements CookStrategy { @Override public void cookFish() { System.out.println("begin to cook steamed fish."); System.out.println("ding! you have a steamed fish."); } } // 烤魚 public class GrillFish implements CookStrategy { @Override public void cookFish() { System.out.println("begin to grill fish."); System.out.println("ding! you have a grill fish."); } } // 策略管理 Context public class CookContext {
private CookStrategy cookStrategy;
public CookContext(CookStrategy cookStrategy) {
this.cookStrategy = cookStrategy;
}
public void cookFish() {
cookStrategy.cookFish();
}
} ```
在定義好烹飪方式策略和策略管理之後,就是編寫呼叫方的呼叫程式碼了,當我們想做一條清蒸魚時,只需在 CookContext 的構造方法裡面傳入具體的清蒸魚策略即可
java
public static void main(String[] args) {
CookContext cookContext = new CookContext(new SteamedFish());
cookContext.cookFish();
// 控制檯輸出
// begin to cook steamed fish.
// ding! you have a steamed fish.
}
同理,當我們想做一條烤魚時也是傳入具體的烤魚策略
java
public static void main(String[] args) {
CookContext cookContext = new CookContext(new GrillFish());
cookContext.cookFish();
// 控制檯輸出
// begin to grill fish.
// ding! you have a grill fish.
}
那麼有人可能會有疑問,如果我們新增添加了更多的烹飪方式比如酸菜魚、水煮魚等等,那麼方式越來越多,客戶端所管理的策略也會越來越多,而我們的的具體策略選擇不就又回到了呼叫者身上了嗎?這個時候就要使用策略模式的擴充套件——策略工廠了。
2. 策略工廠
2.1 減輕客戶端的負擔
當我們新增的烹飪魚的方式越來越多的時候就需要根據條件來選擇具體的烹飪方式,客戶端呼叫程式碼就會變成:
java
public static void main(String[] args) {
CookContext cookContext = null;
String cook = "grill";
if ("grill".equals(cook)) {
cookContext = new CookContext(new GrillFish());
} else if ("steamd".equals(cook)) {
cookContext = new CookContext(new SteamedFish());
} else if ("shuizhu".equals(cook)) {
cookContext = new CookContext(new ShuizhuStrategy());
} else {
cookContext = new CookContext(new SuancaiStrategy());
}
cookContext.cookFish();
}
現在能看到客戶端的判斷越來越複雜,因此結合簡單工廠模式(可參考部落格:【深入設計模式】工廠模式—簡單工廠和工廠方法),將判斷語句下沉到 Context 中,呼叫者便不在進行條件判斷,而是隻用傳入引數即可。
2.2 策略工廠寫法
Context 的程式碼如下:
```java public class CookContext {
private CookStrategy cookStrategy;
public CookContext(String key) {
if ("grill".equals(key)) {
cookStrategy = new GrillFish();
} else if ("steamd".equals(key)) {
cookStrategy = new SteamedFish();
} else if ("shuizhu".equals(key)) {
cookStrategy = new ShuizhuStrategy();
} else {
cookStrategy = new SuancaiStrategy();
}
}
public void cookFish() {
cookStrategy.cookFish();
}
} ```
可以看到在 CookContext 的程式碼中,原來的構造方法引數從 Strategy 改成了具體 Strategy 對應的 key,當我們在構造 CookContext 的時候就會根據 key 構造出對應的具體 Strategy,因此呼叫者的程式碼就變成下面這樣:
java
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext(cook);
cookContext.cookFish();
}
還有一種寫法就是在呼叫 cookFish 時再根據 key 選擇對應具體策略方法,而在構造 CookContext 時僅僅將所用到的策略根據 key 進行快取,程式碼如下:
```java public class CookContext {
private Map<String, CookStrategy> strategyMap;
public CookContext() {
strategyMap = new HashMap<>();
strategyMap.put("grill", new GrillFish());
strategyMap.put("steamd", new SteamedFish());
strategyMap.put("shuizhu", new ShuizhuStrategy());
strategyMap.put("suancai", new SuancaiStrategy());
}
public void cookFish(String key) {
strategyMap.get(key).cookFish();
}
} ```
對應的呼叫者程式碼也改成如下:
java
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext();
cookContext.cookFish(cook);
}
以上兩種寫法都是沒問題的,主要根據個人習慣以及實際場景選擇即可。
回到開篇提出的大量 if else 問題上,當我們遇到判斷分支很多,並且每個分支邏輯複雜時,我們便可以使用策略工廠,將原來每個分支裡面的業務程式碼進行策略封裝,同時使用 Context 將判斷條件和封裝後的策略進行關聯。這樣做的好處是在將來如果再次新增判斷分支時,只需新增策略類即可,呼叫者也不再與具體策略耦合。並且程式碼條理和責任會更清晰,每個分支只會關心自己對應的策略,對策略的修改也不會對呼叫方產生任何影響。
3. 策略模式在框架原始碼中的應用
3.1 策略模式在 JDK 中的應用
ThreadPoolExecutor 類
在我們建立執行緒池時,會呼叫 ThreadPoolExecutor 的建構函式 new 一個物件,在建構函式中需要傳入七個引數,其中有一個引數叫 RejectedExecutionHandler handler 也就是執行緒的拒絕策略。
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
傳入拒絕策略之後將物件賦給 ThreadPoolExecutor 物件的成員變數 handler,在需要對加入執行緒池的執行緒進行拒絕時,直接呼叫 RejectedExecutionHandler 中的 reject 方法即可,方法內部呼叫傳入 handler 的 rejectedExecution 方法。
java
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
但是 RejectedExecutionHandler 是一個介面,也就是說我們需要傳入具體的實現,這裡便是使用的策略模式。RejectedExecutionHandler 介面對應 Strategy 介面,下面四種實現類對應具體策略;RejectedExecutionHandler 對應 Context 類,外部呼叫 RejectedExecutionHandler 的 reject 方法,再由 RejectedExecutionHandler 內部呼叫具體策略實現的方法。
TreeMap
在建立 TreeMap 物件的時候可以在構造方法中傳入 Comparetor 物件來決定 TreeMap 中的 key 是按照怎樣的順尋進行排列。並且 TreeMap 通過提供 compare 方法呼叫比較器的 compare 方法進行兩個引數的比較,因此該處也是使用的策略模式。
```java public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
final int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); } ```
4. 總結
策略模式用於在完成相同工作時有多種不同實現的選擇上,這些實現都能以相同的方式進行呼叫,減少方法實現和方法呼叫上的耦合。在實際開發中使用策略模式不僅能簡化程式碼,而且能夠簡化我們的單元測試。策略模式將策略的選擇交給了呼叫者,從而讓具體策略僅關注自己的實現邏輯。