生活中我們經(jīng)常會遇到選擇問題峡继,比如當我們要出去旅游時,會考慮是自駕匈挖、坐飛機還是坐火車前往目的地碾牌;或者在烹飪一條魚時康愤,是考慮清蒸、水煮還是燒烤舶吗;又或者商家在對商品促銷時征冷,是使用會員累計積分、打折促銷或者買贈的方式進行促銷誓琼。這個時候就需要根據(jù)當前不同的的條件检激,來選擇出對應的具體實現(xiàn)方式,這就是策略模式腹侣。在實際開發(fā)中叔收,策略模式也是會經(jīng)常使用的一種設計模式。在實現(xiàn)某個功能有多種方式可供選擇時筐带,策略模式就能派上用場。
1. 策略模式
1.1 策略模式簡介
不知道在座的各位有沒有在維護項目代碼時缤灵,看到過大段大段的 if else 語句伦籍,本人曾有幸遇到過一個方法里面大量 if else 嵌套,并且每一個代碼塊都很長腮出。這種代碼通常是第一版開發(fā)時判斷分支比較少帖鸦,就是用 if else 來進行處理,隨著版本迭代胚嘲,功能需求的增加作儿,后面為了快速迭代就直接在原來的 if else 語句基礎上繼續(xù)添加判斷分支,久而久之就嵌套出了大量的判斷分支馋劈。這樣寫法雖然開發(fā)的人寫著快攻锰,但是對于后面代碼維護或者新人閱讀代碼是非常不友好,甚至感到崩潰的妓雾。那么當我們在開發(fā)中發(fā)現(xiàn)判斷分支開始膨脹時娶吞,這個時候就可以考慮使用策略模式來進行處理。
策略模式定義了一系列功能的實現(xiàn)械姻,而這些功能實現(xiàn)的目的是相同的妒蛇,能夠使用相同的方式來調(diào)用所有的實現(xiàn),只是調(diào)用時根據(jù)傳入不同參數(shù)從而獲取到不同的實現(xiàn)楷拳。使用這樣的方式將方法調(diào)用和功能實現(xiàn)進行分割绣夺,從而達到具體策略之間相互獨立,修改欢揖、新增策略實現(xiàn)時陶耍,不會對策略調(diào)用方和其他策略產(chǎn)生影響。
1.2 策略模式結(jié)構(gòu)
在簡單了解了策略模式之后她混,我們來看看他的結(jié)構(gòu)物臂。
策略模式中需要定義一個策略接口 Strategy旺拉,使用具體策略類實現(xiàn)該接口來封裝具體的策略實現(xiàn)過程。同時還需要給調(diào)用方提供一個管理 Strategy 配置類 Context棵磷,調(diào)用方通過 Context 來調(diào)用具體的策略蛾狗。
/**
* 策略接口
*/
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();
}
}
調(diào)用方代碼
public static void main(String[] args) {
// 調(diào)用具體策略 1
Context strategyContext1 = new Context(new SpecificStrategy1());
strategyContext1.strategyMethod();
// 調(diào)用具體策略 2
Context strategyContext2 = new Context(new SpecificStrategy2());
strategyContext2.strategyMethod();
}
我們可以看到在這樣的結(jié)構(gòu)下,調(diào)用方只需要在構(gòu)建 Context 的時候傳入具體的策略實現(xiàn)就可以了仪媒,調(diào)用方也不會在關心具體是怎么實現(xiàn)的沉桌。如果要新增策略實現(xiàn)方式,則新增實現(xiàn)類即可算吩,同樣修改實現(xiàn)也只需修改對應的實現(xiàn)類留凭。
1.3 策略模式示例
前面對策略模式的概念和結(jié)構(gòu)進行了介紹,可能還是會感覺有點云里霧里偎巢,下面就用具體示例來加深理解蔼夜。
場景模擬:假設你現(xiàn)在有一條魚準備烹飪,烹飪的方式有清蒸压昼、烤魚兩種做法求冷,那么我們就可以使用策略模式來得到一條烹飪完成的魚。
首先我們定義一個烹飪策略類(CookStrategy)并定義 cookFish 方法窍霞,然后定義兩個具體的策略實現(xiàn)類 SteamedFish (水煮魚)和 GrillFish (烤魚)匠题。根據(jù)結(jié)構(gòu)定義我們還需要給調(diào)用方提供一個 CookContext 類來管理具體的策略和方法調(diào)用,代碼如下:
// 烹飪策略類
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();
}
}
在定義好烹飪方式策略和策略管理之后但金,就是編寫調(diào)用方的調(diào)用代碼了韭山,當我們想做一條清蒸魚時,只需在 CookContext 的構(gòu)造方法里面?zhèn)魅刖唧w的清蒸魚策略即可
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.
}
同理冷溃,當我們想做一條烤魚時也是傳入具體的烤魚策略
public static void main(String[] args) {
CookContext cookContext = new CookContext(new GrillFish());
cookContext.cookFish();
// 控制臺輸出
// begin to grill fish.
// ding! you have a grill fish.
}
那么有人可能會有疑問钱磅,如果我們添加添加了更多的烹飪方式比如酸菜魚、水煮魚等等似枕,那么方式越來越多续搀,客戶端所管理的策略也會越來越多,而我們的的具體策略選擇不就又回到了調(diào)用者身上了嗎菠净?這個時候就要使用策略模式的擴展——策略工廠了禁舷。
2. 策略工廠
2.1 減輕客戶端的負擔
當我們添加的烹飪魚的方式越來越多的時候就需要根據(jù)條件來選擇具體的烹飪方式,客戶端調(diào)用代碼就會變成:
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();
}
現(xiàn)在能看到客戶端的判斷越來越復雜毅往,因此結(jié)合簡單工廠模式(可參考博客:【深入設計模式】工廠模式—簡單工廠和工廠方法)牵咙,將判斷語句下沉到 Context 中,調(diào)用者便不在進行條件判斷攀唯,而是只用傳入?yún)?shù)即可洁桌。
2.2 策略工廠寫法
Context 的代碼如下:
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 的代碼中,原來的構(gòu)造方法參數(shù)從 Strategy 改成了具體 Strategy 對應的 key侯嘀,當我們在構(gòu)造 CookContext 的時候就會根據(jù) key 構(gòu)造出對應的具體 Strategy另凌,因此調(diào)用者的代碼就變成下面這樣:
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext(cook);
cookContext.cookFish();
}
還有一種寫法就是在調(diào)用 cookFish 時再根據(jù) key 選擇對應具體策略方法谱轨,而在構(gòu)造 CookContext 時僅僅將所用到的策略根據(jù) key 進行緩存,代碼如下:
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();
}
}
對應的調(diào)用者代碼也改成如下:
public static void main(String[] args) {
String cook = "grill";
CookContext cookContext = new CookContext();
cookContext.cookFish(cook);
}
以上兩種寫法都是沒問題的吠谢,主要根據(jù)個人習慣以及實際場景選擇即可土童。
回到開篇提出的大量 if else 問題上,當我們遇到判斷分支很多工坊,并且每個分支邏輯復雜時献汗,我們便可以使用策略工廠,將原來每個分支里面的業(yè)務代碼進行策略封裝王污,同時使用 Context 將判斷條件和封裝后的策略進行關聯(lián)罢吃。這樣做的好處是在將來如果再次新增判斷分支時,只需新增策略類即可昭齐,調(diào)用者也不再與具體策略耦合尿招。并且代碼條理和責任會更清晰,每個分支只會關心自己對應的策略阱驾,對策略的修改也不會對調(diào)用方產(chǎn)生任何影響就谜。
3. 策略模式在框架源碼中的應用
3.1 策略模式在 JDK 中的應用
ThreadPoolExecutor 類
在我們創(chuàng)建線程池時,會調(diào)用 ThreadPoolExecutor 的構(gòu)造函數(shù) new 一個對象啊易,在構(gòu)造函數(shù)中需要傳入七個參數(shù)吁伺,其中有一個參數(shù)叫 RejectedExecutionHandler handler 也就是線程的拒絕策略饮睬。
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租谈,在需要對加入線程池的線程進行拒絕時,直接調(diào)用 RejectedExecutionHandler 中的 reject 方法即可捆愁,方法內(nèi)部調(diào)用傳入 handler 的 rejectedExecution 方法割去。
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
但是 RejectedExecutionHandler 是一個接口,也就是說我們需要傳入具體的實現(xiàn)昼丑,這里便是使用的策略模式呻逆。RejectedExecutionHandler 接口對應 Strategy 接口,下面四種實現(xiàn)類對應具體策略菩帝;RejectedExecutionHandler 對應 Context 類咖城,外部調(diào)用 RejectedExecutionHandler 的 reject 方法,再由 RejectedExecutionHandler 內(nèi)部調(diào)用具體策略實現(xiàn)的方法呼奢。
TreeMap
在創(chuàng)建 TreeMap 對象的時候可以在構(gòu)造方法中傳入 Comparetor 對象來決定 TreeMap 中的 key 是按照怎樣的順尋進行排列宜雀。并且 TreeMap 通過提供 compare 方法調(diào)用比較器的 compare 方法進行兩個參數(shù)的比較,因此該處也是使用的策略模式握础。
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. 總結(jié)
策略模式用于在完成相同工作時有多種不同實現(xiàn)的選擇上辐董,這些實現(xiàn)都能以相同的方式進行調(diào)用,減少方法實現(xiàn)和方法調(diào)用上的耦合禀综。在實際開發(fā)中使用策略模式不僅能簡化代碼简烘,而且能夠簡化我們的單元測試苔严。策略模式將策略的選擇交給了調(diào)用者,從而讓具體策略僅關注自己的實現(xiàn)邏輯孤澎。