Strategy Pattern 教你秒變“神槍手”

今天介紹的是?Strategy Pattern (策略模式)。同樣干貨滿滿弹惦,希望看到的你依舊能有所收獲~

? 目的??

做一件事情有不同的實現(xiàn)方式, 可以將變化的部分和不變的部分剝離開, 去除大量的 if/else, 提供高擴展性。

例子代碼

比如我們想要帶妹吃雞, 就要成為一個神槍手悄但。在各種槍戰(zhàn)游戲中, 有各種不同的槍, 我們要根據(jù)實際情況(比如肤频,射程)的不同選擇不同的槍進行射擊。

如果槍的子彈數(shù)量都不富裕, 我們要用最少的子彈算墨,最合適的方法達到最強傷害, 最終大吉大利。這樣汁雷,你想帶妹子去哪里吃雞就去哪里吃雞啦净嘀, 真的是哪里都可以哦报咳。

當我們距離對手:

1米以內(nèi),使用平底鍋( 想我當時三級頭三級甲, 手持 AKM, 滿血滿狀態(tài), 三級包里藥包無數(shù), 到了決賽圈被平底鍋堵在墻角打死啦?? )挖藏;

100 米左右暑刃,使用沖鋒槍;

超過1000 米膜眠,使用狙擊槍(對于想我醬紫的小菜雞岩臣,基本流程是開一槍沒打中, 暴露位置, 被別人一狙打死...囧)。

/**

* 面條式代碼判斷最強武器

*/

publicclassNoodlesKillProcessor{

/**

* 根據(jù)距離判斷最好的武器擊殺對手

*@paramdistance

*/

* 根據(jù)距離判斷最好的武器擊殺對手

*@paramdistance

*/

@BadSmell

publicstaticvoidkillByDistance(intdistance){

????????if(distance <0) {

????????????thrownewRuntimeException

("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢");

? ? ? ?} ? ? ? ?

????????if(distance >=0&& distance <1) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("二步快速走過去");

System.out.println("掏出平底鍋呼死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=1&& distance <10) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("快速走過去");

System.out.println("掏出手槍打死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=10&& distance <100) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("身體站直, 心態(tài)穩(wěn)住");

System.out.println("掏出沖鋒槍打死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=100&& distance <1000) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("身體蹲下降低后坐力");

System.out.println("掏出步槍");

System.out.println("打開 3 倍鏡");

System.out.println("開槍射擊");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=1000) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("趴在草叢里茍著");

System.out.println("掏出狙擊槍");

System.out.println("打開 8 倍鏡");

System.out.println("開槍射擊");

????????????return;

}

}

}

問題分析

我們覺得這有3個問題宵膨,具體分析如下:

01

可讀性問題

我看這么多 if/else 語句, 里面的 sout 語句目前三四行也還好, 如果我們有上百行的語句, 里面也有很多 if/else, 這樣都不知道下個主 if 跑哪去啦??

02

重復性問題

全都需要發(fā)現(xiàn)敵人, 如果發(fā)現(xiàn)敵人是個成百上千行代碼, 就很麻煩啦架谎。

03

可維護性問題

如果這時候我們新增了一種槍, 比如是散彈槍, 適用10 到20 的時候使用, 這時候我們就需要在加一個 if 語句如下:

/**

* 面條式代碼判斷最強武器

*/publicclassNoodlesKillProcessor{

/**

* 根據(jù)距離判斷最好的武器擊殺對手

*@paramdistance

*/

* 根據(jù)距離判斷最好的武器擊殺對手

*@paramdistance

*/

@BadSmell

publicstaticvoidkillByDistance(intdistance){

????????if(distance <0) {

????????????thrownewRuntimeException

("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢");

? ? ? ?} ? ? ? ?

????????if(distance >=0&& distance <1) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("二步快速走過去");

System.out.println("掏出平底鍋呼死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=1&& distance <10) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("快速走過去");

System.out.println("掏出手槍打死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=10&& distance <20) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("身體站直, 瞄準");

System.out.println("打一槍算一槍");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=20&& distance <100) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("身體站直, 心態(tài)穩(wěn)住");

System.out.println("掏出沖鋒槍打死他");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=100&& distance <1000) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("身體蹲下降低后坐力");

System.out.println("掏出步槍");

System.out.println("打開 3 倍鏡");

System.out.println("開槍射擊");

????????????return;

? ? ? ?} ? ? ? ?

????????if(distance >=1000) {

System.out.println("發(fā)現(xiàn)敵人");

System.out.println("趴在草叢里茍著");

System.out.println("掏出狙擊槍");

System.out.println("打開 8 倍鏡");

System.out.println("開槍射擊");

????????????return;

}

}

}

這個看著也沒啥大問題的樣子, 不就是加了個 if 么, 但是由于我們改動了這個文件, 測試同學問我們需要測試哪些功能, 說是測一種槍需要 5 天???♂?

問題來啦, 本來說是你增加一種槍, 需要測 5 天, 但是現(xiàn)在你說改了這文件, 上下可能有些局部變量共享的, 或者有些方法可能改了入?yún)⒌闹? 這些有負作用的方法被調(diào)用啦, 所以可能狙擊槍也得測一測, 可能手槍也得測一測。

測試同學崩了, 本來 5 天的工作量, 搞成了 5 * 6 天, 一個月都在測槍??, KPI 都沒啦, OKR 都沒啦, 錢都沒啦, 老婆都跑啦, 自己都綠啦等等等, 不拿刀砍你就是真感情啦??

初步嘗試解決

我們先定義好一個基礎(chǔ)類, 解決一下可讀性問題和重復性問題辟躏。

定義一個基礎(chǔ)武器類:

/**

* 抽象的槍

*/

publicabstractclassWeapon{

/**

* 發(fā)現(xiàn)敵人

*/

* 發(fā)現(xiàn)敵人

*/

protectedvoidfindEnemy(){

System.out.println("發(fā)現(xiàn)敵人");

? ?} ? ?

/**

* 開槍前的動作

*/

* 開槍前的動作

*/

protectedabstractvoidpreAction();

/**

* 開槍

*/

* 開槍

*/

protectedabstractvoidshoot();

/**

* 殺人的動作

*/

* 殺人的動作

*/

publicvoidkill(){

findEnemy();

preAction();

shoot();

}

}

逐個實現(xiàn)武器的具體類, 平底鍋, 沖鋒槍, 步槍等類如下:

/**

* 平底鍋

*/

publicclassPanextendsWeapon{

????@Override

protectedvoidpreAction(){

System.out.println("二步快速走過去");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("掏出平底鍋呼死他");

}

}

/**

* 手槍類

*/

publicclassPistolextendsWeapon{

????@Override

protectedvoidpreAction(){

System.out.println("快速走過去");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("掏出手槍打死他");

}

}

/**

* 散彈槍

*/

publicclassShotgunextendsWeapon{

????@Override

protectedvoidpreAction(){

System.out.println("身體站直, 瞄準");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("打一槍算一槍");

}

}

/**

* 狙擊槍

*/

publicclassSniperRifleextendsWeapon{

????@Override

protectedvoidpreAction(){

System.out.println("趴在草叢里茍著");

System.out.println("掏出狙擊槍");

System.out.println("打開 8 倍鏡");

}@Override

protectedvoidshoot(){

System.out.println("開槍射擊");

}

}

/**

* 沖鋒槍

*/

publicclassSubmachineGunextendsWeapon{

????@Override

protectedvoidpreAction(){

System.out.println("身體站直, 心態(tài)穩(wěn)住");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("掏出沖鋒槍打死他");

}

}

我們的方法就可以改動得更清晰啦

/**

* 抽象出類代碼判斷最強武器

*/

publicclassWeaponKillProcessor{

* 根據(jù)距離判斷最好的武器擊殺對手

*

*@paramdistance

*/

@BadSmell

publicstaticvoidkillByDistance(intdistance){

????????if(distance <0) {

????????????thrownewRuntimeException

("距離咋還能是負數(shù)呢? 我以為只要我能 -20 cm 呢?");

? ? ? ?}

Weapon weapon =null;

????????if(distance >=0&& distance <1) {

weapon =newPan();

}elseif(distance >=1&& distance <10) {

weapon =newPistol();

}elseif(distance >10&& distance <20) {

weapon =newShotgun();

}elseif(distance >=20&& distance <100) {

weapon =newSubmachineGun();

}elseif(distance >=100&& distance <1000) {

weapon =newRifle();

}elseif(distance >=1000) {

weapon =newSniperRifle();

}

weapon.kill();

}

}

類圖如下:

使用策略模式

上面的代碼沒有解決最根本的問題, 也就是去除 if/else, 所用的方法其實就是將 if else 轉(zhuǎn)換為 for, 這樣的代碼后續(xù)添加槍就不需要再增加新的類型啦谷扣。

我們先定義一個通用的策略模式接口如下:

/**

* 策略模式

*/publicinterfaceStrategy

RextendsAbstractStrategyResponse>{

* 執(zhí)行策略

*@paramrequest

*@return

*/

RexecuteStrategy(T request);

}

入?yún)⒑统鰠⒍际腔镜某橄箢悾?/p>

/**

* 策略模式抽象入?yún)?/p>

*/

publicabstractclassAbstractStrategyRequest{

}

/**

* 策略模式抽象出參

*/

publicabstractclassAbstractStrategyResponse{

}

實現(xiàn)一個武器抽象類實現(xiàn)接口:

publicabstractclassWeaponStrategyimplements

Strategy

AbstractStrategyResponse>{

/**

* 發(fā)現(xiàn)敵人

*/

protectedvoidfindEnemy(){

System.out.println("發(fā)現(xiàn)敵人");

? ?} ? ?

/**

* 開槍前的動作

*/

protectedabstractvoidpreAction();

/**

* 開槍

*/

protectedabstractvoidshoot();

/**

* 獲取距離范圍

*@return

*/

protectedabstractRangequeryDistanceRange();

/**

* 殺人的動作

*/

publicvoidkill(){

findEnemy();

preAction();

shoot();

? ?} ? ?

????@Override

publicAbstractStrategyResponse

????????executeStrategy(WeaponStrategyRequest request){

System.out.println("距離敵人 "+

????????????????????????????????request.getDistance());

? ? ? ?kill(); ? ? ? ?

????????returnnull;

}

}

其中的 Range 類實現(xiàn)如下:

/**

* 范圍類

*@param

*/

@Data

@AllArgsConstructor

publicclassRange>{

????privateT start;

????privateT end;

????publicRange(T start, T end){

????????this.start = start;

????????this.end = end;

? ?} ? ?

????privatebooleanisIncludeStart =true;

????privatebooleanisIncludeEnd =false;

/**

* 判斷是否在范圍內(nèi)

*@paramtarget

*@return

*/

publicbooleaninRange(T target){

????????if(isIncludeStart) {

????????????if(start.compareTo(target) >0) {

????????????????returnfalse;

? ? ? ? ? ?}

}else{

????????????if(start.compareTo(target) >=0) {

????????????????returnfalse;

? ? ? ? ? ?}

? ? ? ?} ? ? ? ?

????????if(isIncludeEnd) {

????????????if(end.compareTo(target) <0) {

????????????????returnfalse;

? ? ? ? ? ?}

}else{

????????????if(end.compareTo(target) <=0) {

????????????????returnfalse;

? ? ? ? ? ?}

? ? ? ?} ? ? ? ?

????????returntrue;

}

}

依次實現(xiàn)這個抽象武器策略類:

/**

* 平底鍋

*/

publicclassPanStrategyextendsWeaponStrategy{@Override

protectedvoidpreAction(){

System.out.println("二步快速走過去");

}@Override

protectedvoidshoot(){

System.out.println("掏出平底鍋呼死他");

}@Override

protectedRangequeryDistanceRange(){returnnewRange<>(0,1);

}

}

/**

* 手槍類

*/

publicclassPistolStrategyextendsWeaponStrategy{

????@Override

protectedvoidpreAction(){

System.out.println("快速走過去");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("掏出手槍打死他");

? ?} ? ?

????@Override

protectedRangequeryDistanceRange(){

????????returnnewRange<>(1,10);

}

}

/**

* 步槍

*/

publicclassRifleStrategyextendsWeaponStrategy{

????@Override

protectedvoidpreAction(){

System.out.println("身體蹲下降低后坐力");

System.out.println("掏出步槍");

System.out.println("打開 3 倍鏡");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("開槍射擊");

? ?} ? ?

????@Override

protectedRangequeryDistanceRange(){

????????returnnewRange<>(100,1000);

}

}

/**

* 散彈槍

*/

publicclassShotgunStrategyextendsWeaponStrategy{

????@Override

protectedvoidpreAction(){

System.out.println("身體站直, 瞄準");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("打一槍算一槍");

? ?} ? ?

????@Override

protectedRangequeryDistanceRange(){

????????returnnewRange<>(10,20);

}

}

publicclassSniperRifleStrategyextendsWeaponStrategy{

????@Override

protectedvoidpreAction(){

System.out.println("趴在草叢里茍著");

System.out.println("掏出狙擊槍");

System.out.println("打開 8 倍鏡");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("開槍射擊");

? ?} ? ?

????@Override

protectedRangequeryDistanceRange(){

????????returnnewRange<>(1000, Integer.MAX_VALUE);

}

}

/**

* 沖鋒槍

*/

publicclassSubmachineGunStrategyextendsWeaponStrategy{

????@Override

protectedvoidpreAction(){

System.out.println("身體站直, 心態(tài)穩(wěn)住");

? ?} ? ?

????@Override

protectedvoidshoot(){

System.out.println("掏出沖鋒槍打死他");

? ?} ? ?

????@Override

protectedRangequeryDistanceRange(){

????????returnnewRange<>(20,100);

}

}

定義一個上下文類來對入?yún)⑦M行路由:

/**

* 策略上下文, 用來路由策略

*/

public class StrategyContext {

? ?public static final List<WeaponStrategy>

????????WEAPON_STRATEGYS = new ArrayList<>();

static {

WEAPON_STRATEGYS.add(new PanStrategy());

WEAPON_STRATEGYS.add(new PistolStrategy());

WEAPON_STRATEGYS.add(new RifleStrategy());

WEAPON_STRATEGYS.add(new ShotgunStrategy());

WEAPON_STRATEGYS.add(new SniperRifleStrategy());

WEAPON_STRATEGYS.add(new SubmachineGunStrategy());

}

public static void execute(Integer distance) {

? ? ? ?WEAPON_STRATEGYS.stream().

????????filter((weaponStrategy -> {

? ? ? ? ? ?Range<Integer> integerRange =

????????????????weaponStrategy.queryDistanceRange();

? ? ? ? ? ?return integerRange.inRange(distance);

? ? ? ?})).

????????findAny().

????????get().

????????executeStrategy(

????????????new WeaponStrategyRequest(distance));

}

}

最后在主方法里面調(diào)用就好啦:

publicclassApp{

publicstaticvoidmain(String[] args){

????????intdistance =89;

? ? ? ?StrategyContext.execute(89);

}

}

結(jié)果如下:

距離敵人 89

發(fā)現(xiàn)敵人

身體站直, 心態(tài)穩(wěn)住

掏出沖鋒槍打死他

類圖如下:

?其他

這個是比較常見的方法, 基于Spring 的實現(xiàn)和基于反射的實現(xiàn), 還有生產(chǎn)上的例子會在習題中給出。

策略者模式課后作業(yè)

1.了解使用反射和在 spring 框架下策略模式的使用, 將文中的例子轉(zhuǎn)為這二種情況下的代碼捎琐。

2. 比較利用運行時動態(tài)決策策略和初始化的時候就將策略整理到一個集合下的性能差異会涎。

3. 作為一個銀行開發(fā)人員, 有一天產(chǎn)品經(jīng)理需要你完成借貸優(yōu)惠卷計算的部分邏輯, 大致需求如下:

貸款分為本金利息, 輸入一種貸款屬性 (本金 + 利息) 和優(yōu)惠卷類型, 需要你計算用戶總共應(yīng)該還的金額從而給用戶推薦最省錢的優(yōu)惠卷

貸款屬性類定義如下:

/**

* 貸款金額屬性

*/

@DatapublicclassLoanAmtDetail{

/**

* 本金

*/

privateBigDecimal priceAmt;

/**

* 利息

*/

privateBigDecimal rateAmt;

}

優(yōu)惠卷類型如下:

一. 利息全免, 本金不變

二. 利息打 8 折, 本金不變

三. 利息固定抵扣抵扣 800 元, 本金不變

四. 本金加利息一起打 9 折

請使用策略模式實現(xiàn)。

這時候產(chǎn)品提出了一個新的優(yōu)惠卷, 要求本金抵扣 80 元, 利息全免, 我們的改動有多大,測試回歸測試的范圍有多大??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瑞凑,一起剝皮案震驚了整個濱河市末秃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌籽御,老刑警劉巖练慕,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異篱蝇,居然都是意外死亡贺待,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門零截,熙熙樓的掌柜王于貴愁眉苦臉地迎上來麸塞,“玉大人,你說我怎么就攤上這事涧衙∧墓ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵弧哎,是天一觀的道長雁比。 經(jīng)常有香客問我,道長撤嫩,這世上最難降的妖魔是什么偎捎? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上茴她,老公的妹妹穿的比我還像新娘寻拂。我一直安慰自己,他們只是感情好丈牢,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布祭钉。 她就那樣靜靜地躺著,像睡著了一般己沛。 火紅的嫁衣襯著肌膚如雪慌核。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天申尼,我揣著相機與錄音垮卓,去河邊找鬼。 笑死晶姊,一個胖子當著我的面吹牛扒接,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播们衙,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼钾怔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蒙挑?” 一聲冷哼從身側(cè)響起宗侦,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忆蚀,沒想到半個月后矾利,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡馋袜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年男旗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欣鳖。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡察皇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泽台,到底是詐尸還是另有隱情什荣,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布怀酷,位于F島的核電站稻爬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜕依。R本人自食惡果不足惜桅锄,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一琉雳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竞滓,春花似錦咐吼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厢塘。三九已至茶没,卻和暖如春搅吁,著一層夾襖步出監(jiān)牢的瞬間禽额,已是汗流浹背狂鞋。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工缰贝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哀澈,地道東北人吠式。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓奏窑,卻偏偏與公主長得像扭粱,于是被迫代替她去往敵國和親糕簿。 傳聞我的和親對象是個殘疾皇子探入,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348