【深入設計模式】策略模式—策略模式詳解及策略模式在源碼中的應用

生活中我們經(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)邏輯孤澎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末届氢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亥至,更是在濱河造成了極大的恐慌悼沈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姐扮,死亡現(xiàn)場離奇詭異絮供,居然都是意外死亡,警方通過查閱死者的電腦和手機茶敏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門壤靶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惊搏,你說我怎么就攤上這事贮乳。” “怎么了恬惯?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵向拆,是天一觀的道長。 經(jīng)常有香客問我酪耳,道長浓恳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任碗暗,我火速辦了婚禮颈将,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘言疗。我一直安慰自己晴圾,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布噪奄。 她就那樣靜靜地躺著死姚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勤篮。 梳的紋絲不亂的頭發(fā)上都毒,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音叙谨,去河邊找鬼温鸽。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的涤垫。 我是一名探鬼主播姑尺,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蝠猬!你這毒婦竟也來了切蟋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤榆芦,失蹤者是張志新(化名)和其女友劉穎柄粹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匆绣,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡驻右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了崎淳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堪夭。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拣凹,靈堂內(nèi)的尸體忽然破棺而出森爽,到底是詐尸還是另有隱情,我是刑警寧澤嚣镜,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布爬迟,位于F島的核電站,受9級特大地震影響菊匿,放射性物質(zhì)發(fā)生泄漏付呕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一捧请、第九天 我趴在偏房一處隱蔽的房頂上張望凡涩。 院中可真熱鬧棒搜,春花似錦疹蛉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至克蚂,卻和暖如春闺鲸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背埃叭。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工摸恍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓立镶,卻偏偏與公主長得像壁袄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子媚媒,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355