JAVA設(shè)計模式之策略模式

前言

本系列文章參考《設(shè)計模式之禪》、菜鳥教程網(wǎng)以及網(wǎng)上的一些文章進(jìn)行歸納總結(jié),并結(jié)合自身開發(fā)應(yīng)用。設(shè)計模式的命名以《設(shè)計模式之禪》為準(zhǔn)衔蹲。

設(shè)計模式僅是一些開發(fā)者在日常開發(fā)中的編碼技巧的匯總并非固定不變离赫,可根據(jù)項目業(yè)務(wù)實際情況進(jìn)行擴(kuò)展和應(yīng)用芭逝,切不可被這個束縛。更不要為了使用而使用渊胸,設(shè)計模式是一把雙刃劍旬盯,過度的設(shè)計會導(dǎo)致代碼的可讀性下降,代碼的體積增加翎猛。

系列文章不會詳細(xì)介紹設(shè)計模式的《七大原則》胖翰,也不會對設(shè)計模式進(jìn)行分類。這樣只會增加學(xué)習(xí)和記憶的成本切厘,也會導(dǎo)致使用時的思想固化萨咳,總在想這么設(shè)計是否符合xx原則,是否是xx設(shè)計模式疫稿,xx模式是什么類型等等培他,不是本系列文章的所希望看到的,目標(biāo)只有一個遗座,結(jié)合日常開發(fā)分享自身的應(yīng)用舀凛,以提供一種代碼優(yōu)化的思路。

學(xué)習(xí)然后忘記员萍,也許是一種最好的方式腾降。

就像俗話說的那樣:天下本沒有路,走的人多了碎绎,就變成了路螃壤。在我看來,設(shè)計模式也一樣筋帖,它并非是一種定律奸晴,而是前輩們總結(jié)下來的經(jīng)驗,我們學(xué)習(xí)并結(jié)合實際加以利用日麸,而不是生搬硬套寄啼。

定義

官腔:定義一組算法,將每個算法都封裝起來代箭,并且使它們之間可以互換墩划。

人話:有一個公共的邏輯接口或抽象類,且有一個控制類對實現(xiàn)了公共邏輯接口的類進(jìn)行控制嗡综,在引用時知道該調(diào)用哪一個接口的策略類乙帮。

應(yīng)用場景

樣例1

為了方便沒有了解過設(shè)計模式的小白快速了解,這里放一個網(wǎng)上到處都是的計算器模型极景。

在一個簡單的計算器中我們需要關(guān)注計算器的運算符即可察净。我們假定這個計算器非常非常簡單驾茴,僅支持加減乘除。

先定義一個運算符接口氢卡。

public interface IOperator {
    /**
     * 計算邏輯
     * @return 結(jié)果
     */
    int doOperator(int num1, int num2);
}

代碼比較簡單锈至,只做整形的計算。

加法實現(xiàn):

public class AddOperator implements IOperator{

    @Override
    public int doOperator(int num1, int num2) {
        return num1 + num2;
    }
}

乘法實現(xiàn):

public class MultiplyOperator implements IOperator{
    @Override
    public int doOperator(int num1, int num2) {
        return num1 * num2;
    }
}

減法和除法就不寫了译秦,參考上面即可峡捡。

上面這些IOperator的實現(xiàn)即為不同的運算策略。光有策略還不行诀浪,我們需要一個控制類棋返,對其進(jìn)行統(tǒng)一管理。所有的引用通過這個管理類來實現(xiàn)雷猪。管理類的實現(xiàn)方式有很多種,千萬別局限于網(wǎng)上的一兩個demo晰房。

方式一:

通過構(gòu)造函數(shù)將策略(IOperator)注入到管理類屬性中求摇,然后直接調(diào)用exec執(zhí)行即可。

demo中相當(dāng)于構(gòu)建了一個加法的控制類殊者,然后再執(zhí)行傳參和運算与境。

//控制類
public class StrategyManager_1 {
    private final IOperator operator;

    public StrategyManager_1(IOperator operator) {
        this.operator = operator;
    }
    
    public int exec(int num1, int num2){
        return operator.doOperator(num1, num2);
    }
    
    //引用方式
    public static void main(String[] args) {
        AddOperator operator = new AddOperator();
        StrategyManager_1 manager_1 = new StrategyManager_1(operator);
        int exec = manager_1.exec(1, 2);
        System.out.println(exec);
    }
}

方式二:

還可以使用switch語句,通過輸入的運算符進(jìn)行匹配計算猖吴。

public class StrategyManager_2 {

    public static int exec(int num, int num1, String operator) {
        switch (operator) {
            case "+" :
                return new AddOperator().doOperator(num, num1);
            case "*" :
                return new MultiplyOperator().doOperator(num, num1);
            default:
                return 0;
        }
    }

    public static void main(String[] args) {
        StrategyManager_2.exec(1, 2, "+");
    }
}

這種方式不需要使用者去構(gòu)建策略對象摔刁,有點類似于簡單的工廠模式,只需要傳入一個標(biāo)識即可海蔽,且調(diào)用方式更加簡單共屈,個人更推薦這種方式。實際開發(fā)中也不建議讓使用者自己去封裝策略對象党窜,即增加了調(diào)用者的學(xué)習(xí)成本拗引,也可能會引起各種意想不到的錯誤。要考慮后人是在你的基礎(chǔ)上開發(fā)的幌衣,他們不知道也不需要知道你的策略對象矾削,因為策略對象是你的業(yè)務(wù)的封裝,而使用者并不知道豁护。

真實情況下的計算器哼凯,難道要用戶去創(chuàng)建一個處理加法的策略對象AddOperator嗎?顯然不可能楚里,用戶只關(guān)系輸入的數(shù)字断部、運算符和結(jié)果。

實現(xiàn)的方式可能還有很多腻豌,待各位自行發(fā)現(xiàn)家坎。

樣例2

跟大家分享一下嘱能,在一個springboot項目中,如何編寫一個策略模式的接口虱疏。

最近正好寫過一個對外開放的接口惹骂,用于接受其他項目同步來的用戶信息。但是不同的項目同步來的用戶信息的結(jié)構(gòu)和內(nèi)容是不同的做瞪,有些需要進(jìn)行定制業(yè)務(wù)開發(fā)对粪。

以此背景為例,假設(shè)有兩個項目(A 和 B)需要同步數(shù)據(jù)給我装蓬。

條件:

1.A項目傳入的僅有用戶姓名和手機(jī)號著拭,B項目傳入的僅有身份證和姓名。

2.A項目需要接受完成后同步發(fā)送短信牍帚,B項目需要同步的時候根據(jù)身份證計算年齡和生日儡遮。

3.任何一種情況同步前和完成后都需要記錄日志。

1.創(chuàng)建入?yún)ο?/strong>:

//剝離出來公共的參數(shù)
@Data
public class ReceiveBean {
    private String name;
    /**
    * 來源
    */
    private int source;
}

//B項目同步來的參數(shù)
@EqualsAndHashCode(callSuper = true)
@Data
public class BReceiveBean extends ReceiveBean{
    private String idNo;
}

//A項目同步來的參數(shù)
@EqualsAndHashCode(callSuper = true)
@Data
public class AReceiveBean extends ReceiveBean{
    private String phone;
}

實體bean中使用了lombok相關(guān)的注解代替了顯示的get/set方法暗赶。

ReceiveBean中有一個關(guān)鍵的source字段用來區(qū)別渠道的來源毙驯,可用其他的方式代替锌雀。

2.定義頂層的策略接口

public interface IReceiveService {

    /**
     * 接受的參數(shù)為json格式的字符串
     * 由調(diào)用者進(jìn)行轉(zhuǎn)換
     * @param bean入?yún)?     */
    void receive(ReceiveBean bean);

}

使用字符串或Map對象能提供更好的兼容性蕴茴,也可以在內(nèi)部進(jìn)行轉(zhuǎn)換玉罐,這里為了方便外部的引用將轉(zhuǎn)換的過程轉(zhuǎn)交給調(diào)用者。

3.抽象層面接口實現(xiàn)

@SuppressWarnings("unused")
public abstract class AbstractReceiveService implements IReceiveService{

    //可通過覆蓋這個方法定制接受之前的處理岳锁,替換bean的屬性
    protected void receivePre(ReceiveBean bean) {
        //記錄日志
    }

    //通過覆蓋這個方法定制接受之后的處理
    //這個可能會不滿足绩衷,若需要的話可以添加一個map類型的參數(shù),用來保存處理過程中產(chǎn)生數(shù)據(jù)
    protected void receiveAfter(ReceiveBean bean) {
        //記錄日志
    }

    //需要被重寫,各類型的定制服務(wù)
    protected abstract void doAction(ReceiveBean bean);

    @Override
    public void receive(ReceiveBean bean) {
        //業(yè)務(wù)開始處理之前的操作
        receivePre(bean);
        //業(yè)務(wù)開始處理
        doAction(bean);
        //業(yè)務(wù)處理之后的操作
        receiveAfter(bean);
    }
}

抽象類內(nèi)部實現(xiàn)了receive方法激率,這里要看具體的業(yè)務(wù)咳燕,雖然有定制的服務(wù),但總體邏輯相同的可以這么實現(xiàn)柱搜,若差別較大迟郎,可在子類中覆蓋receive方法,自行實現(xiàn)聪蘸,否則就默認(rèn)走相同的流程宪肖。

整體還算比較簡單,receive方法內(nèi)進(jìn)行了參數(shù)的轉(zhuǎn)換健爬,并且調(diào)用了receivePrereceiveAfter進(jìn)行業(yè)務(wù)前后的處理控乾,兩個方法內(nèi)部都做了日志保存,這屬于默認(rèn)操作娜遵,也可以通過覆蓋這兩個方法進(jìn)行業(yè)務(wù)和參數(shù)的定制蜕衡,可以影響后續(xù)的處理。doAction方法可以酌情處理设拟,我將其作為一個抽象方法慨仿,要求子類必須去實現(xiàn)各自的業(yè)務(wù)處理操作久脯,也可以和receivePrereceiveAfter連個方法一樣提供默認(rèn)實現(xiàn)。

拿其中一個A項目的實現(xiàn)類看一下:

@Service
public class AReceiveService extends AbstractReceiveService{

    @Override
    protected void doAction(ReceiveBean bean) {
        //保存記錄
        // 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
        AReceiveBean ab = (AReceiveBean) bean;
    }


    @Override
    protected void receivePre(ReceiveBean bean) {
        super.receivePre(bean);
        //記錄日志的同時進(jìn)行其他操作
        // 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
        AReceiveBean ab = (AReceiveBean) bean;
    }

    @Override
    protected void receiveAfter(ReceiveBean bean) {
        super.receiveAfter(bean);
        //發(fā)送短信
        // 使用的時候需要對bean進(jìn)行強(qiáng)轉(zhuǎn)
        AReceiveBean ab = (AReceiveBean) bean;
    }
}

內(nèi)部可選擇性的覆蓋方法镰吆,若非必要的話帘撰。

4.控制類

@SuppressWarnings("unused")
public enum ReceiveSourceEnum {
    A(1, "aReceiveService") {
        @Override
        public ReceiveBean coverJson(String json) {
            return JSON.parseObject(json, AReceiveBean.class);
        }
    },
    B(2, "bReceiveService") {
        @Override
        public ReceiveBean coverJson(String json) {
            return JSON.parseObject(json, BReceiveBean.class);
        }
    };

    private final int source;

    private final String serviceName;

    /**
     * 在此定制轉(zhuǎn)換成不同類型的bean入?yún)?     *
     * @param json json格式入?yún)?     * @return 結(jié)果
     */
    public abstract ReceiveBean coverJson(String json);


    //獲取枚舉資源
    public static ReceiveSourceEnum getWithSource(int source) {
        for (ReceiveSourceEnum sourceEnum : ReceiveSourceEnum.values()) {
            if (sourceEnum.getSource() == source) {
                return sourceEnum;
            }
        }
       throw new DemoException("source類型不存在!");
    }
    //獲取服務(wù)
    public static IReceiveService getReceiveService(int source) {
        ReceiveSourceEnum sourceEnum = getWithSource(source);
        Assert.notNull(sourceEnum, "source is null");
        IReceiveService receiveService = (IReceiveService)SpringContextUtil.getBean(sourceEnum.getServiceName());
        Assert.notNull(sourceEnum, "service is null");
        return receiveService;
    }

    ReceiveSourceEnum(int source, String serviceName) {
        this.source = source;
        this.serviceName = serviceName;
    }

    public int getSource() {
        return source;
    }

    public String getServiceName() {
        return serviceName;
    }
}

ReceiveSourceEnum作為控制類万皿,內(nèi)部提供兩個靜態(tài)的方法getReceiveServicegetWithSource摧找,獲取服務(wù)和枚舉類型。

枚舉類中定義了一個用于將json格式字符串轉(zhuǎn)換成對象的抽象方法coverJson牢硅。因為接受的參數(shù)為json格式字符串蹬耘,這樣可以提高接口的擴(kuò)展性,內(nèi)部轉(zhuǎn)換成對應(yīng)的參數(shù)實體類方便后續(xù)操作减余。

getReceiveService內(nèi)部通過服務(wù)名稱從spring容器中換取bean综苔。這種方式可以靜態(tài)的調(diào)用服務(wù)且方法簡單。

換取的方法網(wǎng)上有很多佳励,這里只提供一種:

@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    //獲取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通過name獲取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通過class獲取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通過name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}

5.引用方法
使用springboot的單元測試休里。

注意:

不能使用main函數(shù)直接引用,否則會發(fā)現(xiàn)SpringContextUtil中的ApplicationContext永遠(yuǎn)都是null赃承,原因是classloader導(dǎo)致的。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestBean {

    @Test
    public void testContextUtil() {
        ReceiveBean bean = new ReceiveBean();
        bean.setName("123");
        bean.setSource(1);
        String paramStr = JSON.toJSONString(bean);
        ReceiveBean receiveBean = coverJsonToBean(paramStr);
        ReceiveSourceEnum.getReceiveService(receiveBean.getSource()).receive(receiveBean);
    }

    private static ReceiveBean coverJsonToBean(String json){
        JSONObject jo = JSONObject.parseObject(json);
        if (!jo.containsKey("source")) {
            throw new DemoException("傳入?yún)?shù)缺失:source");
        }
        //存在強(qiáng)轉(zhuǎn)錯誤的風(fēng)險
        int source = (int)jo.get("source");
        ReceiveSourceEnum sourceEnum = ReceiveSourceEnum.getWithSource(source);
        return sourceEnum.coverJson(json);
    }
}

不過這樣也可以看出來對于使用者來說悴侵,調(diào)用非常簡單瞧剖。若后續(xù)對接了C項目或其他項目,僅需擴(kuò)展AbstractReceiveService類和ReceiveSourceEnum的類型即可可免。

這里僅是我的一種思路抓于,一種參考,肯定還有其他更好的實現(xiàn)方式浇借。

UML圖

圖片來自于菜鳥教程捉撮。(偷懶中= =)

image

小結(jié)

最早了解策略模式是為了優(yōu)化代碼中大量的if-else語句,雖然一些情況下可以通過switch語句和enum類去實現(xiàn)妇垢,但擴(kuò)展性并不好巾遭,且代碼會分散在當(dāng)前業(yè)務(wù)類中,當(dāng)對接方比較少時還可以勉強(qiáng)看闯估。比較多的時候灼舍,就會發(fā)現(xiàn),一個入口方法內(nèi)涨薪,整合了大量的定制業(yè)務(wù)骑素,閱讀和維護(hù)都時分困難,并且業(yè)務(wù)類變得時分龐大刚夺。加之缺少注釋說明献丑,后人維護(hù)起來簡直是噩夢級的末捣。

策略模式下,將不同的渠道提取公共的方法置于父類中创橄,將定制的業(yè)務(wù)在各自的子類中實現(xiàn)箩做,使得整體流程時分清晰,且業(yè)務(wù)類不會無限膨脹筐摘。修改和擴(kuò)展只要不涉及公共部分卒茬,可放心變更,不用擔(dān)心牽一發(fā)而動全身咖熟,每種渠道的定制業(yè)務(wù)相對獨立圃酵。

若對接方完全無法適用公共的部分,也不用擔(dān)心馍管,大可直接覆蓋策略方法郭赐,重寫業(yè)務(wù)邏輯,依然可以保證同一個入口确沸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捌锭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子罗捎,更是在濱河造成了極大的恐慌观谦,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨菜,死亡現(xiàn)場離奇詭異豁状,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)倒得,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門泻红,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霞掺,你說我怎么就攤上這事谊路。” “怎么了菩彬?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵缠劝,是天一觀的道長。 經(jīng)常有香客問我挤巡,道長剩彬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任矿卑,我火速辦了婚禮喉恋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己轻黑,他們只是感情好糊肤,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著氓鄙,像睡著了一般馆揉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抖拦,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天升酣,我揣著相機(jī)與錄音,去河邊找鬼态罪。 笑死噩茄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的复颈。 我是一名探鬼主播绩聘,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耗啦!你這毒婦竟也來了凿菩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帜讲,失蹤者是張志新(化名)和其女友劉穎衅谷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體似将,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡会喝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了玩郊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡枉阵,死狀恐怖译红,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兴溜,我是刑警寧澤侦厚,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站拙徽,受9級特大地震影響刨沦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膘怕,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一想诅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦来破、人聲如沸篮灼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诅诱。三九已至,卻和暖如春送朱,著一層夾襖步出監(jiān)牢的瞬間娘荡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工驶沼, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留炮沐,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓商乎,卻偏偏與公主長得像央拖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鹉戚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容