知道策略模式!但不會在專案裡使用?

2022-12-03 21:00:42

前言

在學開發的第二年就開始聽說要想程式碼寫得好,一定要會設計模式。於是就興致沖沖的啃了《Head First 設計模式》,看完之後對於策略模式映像很深刻,覺得這個模式好,易上手,應用廣,我又能優化一波程式碼了(裝波逼了),於是興致沖沖的開啟了我的 IDEA,開整!!!

策略模式初體驗(錯誤示範)

在講訴我的策略模式首秀前,我們先回顧下策略模式的基本概念。

策略模式

  • 意圖:定義一系列的演演算法,把它們一個個封裝起來, 並且使它們可相互替換。
  • 主要解決:在有多種演演算法相似的情況下,使用 if...else 所帶來的複雜和難以維護。
  • 何時使用:一個系統有許多許多類,而區分它們的只是他們直接的行為。

簡單的來說當做某個事情有多個方式的時候,可以抽象為介面,然後每個實現是一種解決方式,由呼叫方來選擇不同的實現方式。

理解了後我開始對我們的程式碼進行了重構,當時我第一家公司有這樣一段程式碼,大概是這個意思(時間長了,我憑記憶重寫的)。

有這樣一個抽獎的方法,我們後臺控制中獎率,不同的時候我們會調整不同的中獎策略。

public class NumStrategy {

    enum RandomEnum{
        /**
         * 平均策略
         */
        AVERAGE,
        /**
         * 80%的機率中獎
         */
        RANDOM28;
    }

    /**
     * 抽獎方法,根據不同的策略進行抽獎
     * @param randomEnum
     * @return true:代表中獎  false:代表沒中獎
     */
    public boolean luckDraw(RandomEnum randomEnum){
        if(randomEnum.equals(RandomEnum.AVERAGE)){
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 50;
        }else if(randomEnum.equals(RandomEnum.RANDOM28)){
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 20;
        }
        return false;
    }
}

我一看,這不就是妥妥的策略模式嗎?開搞。

一頓改造之後變成了這樣:

public class NumStrategy2 {

    enum RandomEnum{
        /**
         * 平均策略
         */
        AVERAGE,
        /**
         * 80%的機率中獎
         */
        RANDOM28;
    }

    /**
     * 抽獎方法,根據不同的策略進行抽獎
     * @param randomEnum
     * @return true:代表中獎  false:代表沒中獎
     */
    public boolean luckDraw(RandomEnum randomEnum){
        if(randomEnum.equals(RandomEnum.AVERAGE)){
           return new AverageStrategy().luckDraw();
        }else if(randomEnum.equals(RandomEnum.RANDOM28)){
            return new Random28Strategy().luckDraw();
        }
        return false;
    }

    interface LuckDrawStrategy{
        boolean luckDraw();
    }

    class AverageStrategy implements LuckDrawStrategy{

        @Override
        public boolean luckDraw() {
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 50;
        }
    }


    class Random28Strategy implements LuckDrawStrategy{

        @Override
        public boolean luckDraw() {
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 20;
        }
    }

}

改造完成之後我滿意的提交了程式碼,但是在組長 review 的時候給我又改了回來。說你整這麼多類幹嘛?我理直氣壯的說我這是用策略模式優化程式碼。他說沒必要,先改回去吧。

我憤憤的接受了,但心裡想著:哎,你連策略模式都不懂?

經過這麼多年,我開始理解我當時的做法其實不對,本來很簡單的程式碼,而且裡面的邏輯不會有變動,其實不需要抽象出來。我的改動有過度設計之嫌。把原來的 30 行程式碼搞成了 80 行

一報還一報,這幾年我見過太多次當年的我這樣寫程式碼的了。

為了用設計模式而用設計模式。而忘了設計模式的初衷是為了程式碼更易理解,更可靠,更易維護

甚至還見過有人學了策略模式後說要把專案裡所有的 if/else 都安排上策略模式。

梅開二度

又過了一年多,在一次面試的時候,也有著關於策略模式的討論。

【面試官】問:你說你用過策略模式,請問你為什麼用它?

【我】:為了抽離各個不同實現邏輯,優化 if/else,使程式碼更簡單易懂

【面試官】:你具體說說,怎麼去掉的 if/else

【我】:內心 OS(背的知識點,我也好久沒用了啊)。我硬著頭皮說,我可以使用工廠模式+策略模式來做。

【面試官】:那你工廠模式的那裡不是也要用 if/else 判斷嗎?

【我】:。。。額。唔。。。那確實還是要用到 if/else

把我問住了,我支支吾吾的回答確實還是要 if/else 來判斷一次,只不過把判斷移到了工廠模式裡面去了。

我下來後又去實踐了下,想著放在 map 裡行不行呢?

public class NumStrategy3 {

    enum RandomEnum{
        /**
         * 平均策略
         */
        AVERAGE,
        /**
         * 80%的機率中獎
         */
        RANDOM28;
    }

    static Map<RandomEnum,LuckDrawStrategy> map = new HashMap<>();

    static{
        map.put(RandomEnum.RANDOM28,new Random28Strategy());
        map.put(RandomEnum.AVERAGE,new AverageStrategy());
    }

    /**
     * 抽獎方法,根據不同的策略進行抽獎
     * @param randomEnum
     * @return true:代表中獎  false:代表沒中獎
     */
    public boolean luckDraw(RandomEnum randomEnum){
        LuckDrawStrategy luckDrawStrategy = map.get(randomEnum);
        return luckDrawStrategy.luckDraw();
    }

    interface LuckDrawStrategy{
        boolean luckDraw();
    }

    static class AverageStrategy implements LuckDrawStrategy{

        @Override
        public boolean luckDraw() {
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 50;
        }
    }


    static class Random28Strategy implements LuckDrawStrategy{

        @Override
        public boolean luckDraw() {
            Random random = new Random();
            int num = random.nextInt(100);
            return num >= 20;
        }
    }

}

終於是解決了 if/else 的情況,不過這樣很短的 if/else,裡面邏輯不怎麼變動時,我個人是不建議用策略模式,這裡只是範例。

推薦用法

又過了幾年,當初的菜鳥也成長為了一個老鳥。

當時專案裡有這樣一個程式碼:

下面的程式碼我進行了一些簡化,我們有一個功能,對頁面上的指標進行計算,不同的指標對應不同的計算方法。頁面上指標一期做 4 個,後續會做到十幾個。


public interface TransferService {

    String transfer();
}


@Service
public class SearchTransformService {

    @Autowired
    private UserTransferService userTransferService;

    @Autowired
    private AgeTransferService ageTransferService;

    @Autowired
    private InterestTransferService interestTransferService;

    /**
     * 根據不同的編碼進行轉換
     * @param code
     * @return
     */
    public String transform(String code){
        if(code.equals("user")){
            return userTransferService.transfer();
        }else if(code.equals("age")){
            return ageTransferService.transfer();
        }else if(code.equals("interest")){
            return interestTransferService.transfer();
        }
        return "";
    }
}

可以看到這樣的業務場景下,這樣的寫法 if/else 就會很長,後續十幾個的情況下就很難維護。另外 code 使用的是魔數,也是不好的一種寫法。我對此進行了優化如下:

  1. 先將 code 用列舉定義
   enum CodeEnum {
    USER("user"),
    AGE("age"),
    INTEREST("interest"),
    ;

    private String code;

    public String getCode() {
        return code;
    }

    CodeEnum(String code) {
        this.code = code;
    }

    private static final Map<String, CodeEnum> map = Arrays.stream(CodeEnum.values()).collect(Collectors.toMap(CodeEnum::getCode, Function.identity()));


    public CodeEnum of(String code) {
        return map.get(code);
    }
}
  1. 原有的介面上增加一個 transCode 方法,每個實現需要宣告是對應哪個編碼的實現
public interface TransferService {

    String transfer();

    CodeEnum transCode();
}

@Service
public class AgeTransferService implements TransferService {
    @Override
    public String transfer() {
        return null;
    }

    @Override
    public CodeEnum transCode() {
        return CodeEnum.AGE;
    }
}
  1. 使用 map 儲存編碼對應的實現類的關聯關係,以此來獲取對應的轉換器實現類
@Service
public class SearchTransformService implements InitializingBean {

    @Autowired
    private List<TransferService> transferServiceList;

    private Map<CodeEnum, TransferService> transferServiceMap;

    @Override
    // 專案啟動時將實現類放入到map中去
    public void afterPropertiesSet() throws Exception {
        transferServiceMap = transferServiceList.stream().collect(Collectors.toMap(TransferService::transCode, Function.identity()));
    }
    /**
     * 根據不同的編碼進行轉換
     * @param code
     * @return
     */
    public String transform(String code){
        TransferService transferService = transferServiceMap.get(CodeEnum.of(code));
        Assert.notNull(transferService,"找不到對應的轉換器");
        return transferService.transfer();
    }

}

重構後是不是就很簡潔了呢?如果後續新增新的編碼轉換器,只需要先在列舉裡定義,然後新增實現類實現方法就行了,不需要對關心是怎麼呼叫的,只關心具體的實現邏輯,降低了維護成本。

這才是策略模式的真正應用吧。不要再亂用了,哈哈哈。