設計模式最佳實踐探索—策略模式

根據不同的應用場景與意圖,設計模式主要分爲創建型模式、結構型模式和行爲型模式三類。本文主要探索行爲型模式中的策略模式如何更好地應用於實踐中。

前言

在軟件開發的過程中,需求的多變性幾乎是不可避免的,而作爲一名服務端開發人員,我們所設計的程序應儘可能支持從技術側能夠快速、穩健且低成本地響應紛繁多變的業務需求,從而推進業務小步快跑、快速迭代。設計模式正是前輩們針對不同場景下不同類型的問題,所沉澱下來的一套程序設計思想與解決方案,用來提高代碼可複用性、可維護性、可讀性、穩健性以及安全性等。下面是設計模式的祖師爺 GoF(Gang of Four,四人幫)的合影,感受一下大佬的氣質~

靈活應用設計模式不僅可以使程序本身具有更好的健壯性、易修改性和可擴展性,同時它使得編程變得工程化,對於多人協作的大型項目,能夠降低維護成本、提升多人協作效率。根據不同的應用場景與意圖,設計模式主要分爲三類,分別爲創建型模式、結構型模式和行爲型模式。本文主要探索行爲型模式中的策略模式如何更好地應用於實踐中。

使用場景

策略模式屬於對象的行爲模式,其用意是針對一組可替換的算法,將每一個算法封裝到具有共同接口的獨立的類中,使得算法可以在不影響到客戶端(算法的調用方)的情況下發生變化,使用策略模式可以將算法的定義與使用隔離開來,保證類的單一職責原則,使得程序整體符合開閉原則。

以手淘中商詳頁的店鋪卡片爲例,店鋪卡片主要包含店鋪名稱、店鋪 logo、店鋪類型以及店鋪等級等信息,其中不同店鋪類型的店鋪等級計算邏輯是不同的,爲了獲取店鋪等級,可以採用如下所示代碼:

 if (Objects.equals("淘寶", shopType)) {
   // 淘寶店鋪等級計算邏輯
   // return 店鋪等級;
 } else if (Objects.equals("天貓", shopType)) {
   // 天貓店鋪等級計算邏輯
   // return 店鋪等級
 } else if (Objects.equals("淘特", shopType)) {
   // 淘特店鋪等級計算邏輯
   // return 店鋪等級
 } else {
   //  ...
 }

這種寫法雖然實現簡單,但使得各類店鋪等級計算邏輯與程序其他邏輯相耦合,未來如果要對其中一種計算邏輯進行更改或者新增加一種計算邏輯,將不得不對原有代碼進行更改,違背了 OOP 的單一職責原則與開閉原則,讓代碼的維護變得困難。若項目本身比較複雜,去改動項目原有的邏輯是一件非常耗時又風險巨大的事情。此時我們可以採取策略模式來處理,將不同類型的店鋪等級計算邏輯封裝到具有共同接口又互相獨立的類中,其核心類圖如下所示:

這樣一來,程序便具有了良好的可擴展性與易修改性,若想增加一種新的店鋪等級計算邏輯,則可將其對應的等級計算邏輯單獨封裝成 ShopRankHandler 接口的實現類即可,同樣的,若想對其中一種策略的實現進行更改,在相應的實現類中進行更改即可,而不用侵入原有代碼中去開發。

最佳實踐探索

本節仍以店鋪等級的處理邏輯爲例,探索策略模式的最佳實踐。當使用策略模式的時候,會將一系列算法用具有相同接口的策略類封裝起來,客戶端想調用某一具體算法,則可分爲兩個步驟:1、某一具體策略類對象的獲取;2、調用策略類中封裝的算法。比如客戶端接受到的店鋪類型爲 “天貓”,則首先需要獲取 TmShopRankHandleImpl 類對象,然後調用其中的算法進行天貓店鋪等級的計算。在上述兩個步驟中,步驟 2 是依賴於步驟 1 的,當步驟 1 完成之後,步驟 2 也隨之完成,因此上述步驟 1 成爲整個策略模式中的關鍵。

下面列舉幾種策略模式的實現方式,其區別主要在於具體策略類對象獲取的方式不同,對其優缺點進行分析,並探索其最佳實踐。

▐****暴力法

  1. 店鋪等級計算策略接口

    public interface ShopRankHandler {
        /**
        * 計算店鋪等級
        * @return 店鋪等級
        */
        public String calculate();
    }
  2. 各類型店鋪等級計算策略實現類

    淘寶店

    public class TbShopRankHandleImpl implements ShopRankHandler{
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }

    天貓店

    public class TmShopRankHandleImpl implements ShopRankHandler{
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }

    淘特店

    public class TtShopRankHandleImpl implements ShopRankHandler{
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }
  3. 客戶端調用

    // 根據參數調用對應的算法計算店鋪等級
    public String acqurireShopRank(String shopType) {
        String rank = StringUtil.EMPTY_STRING;
        if (Objects.equals("淘寶", shopType)) {
            // 獲取淘寶店鋪等級計算策略類
            ShopRankHandler shopRankHandler = new TbShopRankHandleImpl();
            // 計算店鋪等級
            rank = shopRankHandler.calculate();
        } else if (Objects.equals("天貓", shopType)) {
            // 獲取天貓店鋪等級計算策略類
            ShopRankHandler shopRankHandler = new TmShopRankHandleImpl();
            // 計算店鋪等級
            rank = shopRankHandler.calculate();
        } else if (Objects.equals("淘特", shopType)) {
            // 獲取淘特店鋪等級計算策略類
            ShopRankHandler shopRankHandler = new TtShopRankHandleImpl();
            // 計算店鋪等級
            rank = shopRankHandler.calculate();
        } else {
            //  ...
        }
        return rank;
    }

至此,當我們需要新增策略類時,需要做的改動如下:

  1. 新建策略類並實現策略接口

  2. 改動客戶端的 if else 分支

  1. 將店鋪等級計算邏輯單獨進行封裝,使其與程序其他邏輯解耦,具有良好的擴展性。

  2. 實現簡單,易於理解。

客戶端與策略類仍存在耦合,當需要增加一種新類型店鋪時,除了需要增加新的店鋪等級計算策略類,客戶端需要改動 if else 分支,不符合開閉原則。

▐****第一次迭代(枚舉 + 簡單工廠)

有沒有什麼方法能使客戶端與具體的策略實現類徹底進行解耦,使得客戶端對策略類的擴展實現 “零” 感知?在互聯網領域,沒有什麼問題是加一層解決不了的,我們可以在客戶端與衆多的策略類之間加入工廠來進行隔離,使得客戶端只依賴工廠,而具體的策略類由工廠負責產生,使得客戶端與策略類解耦,具體實現如下所示:

  1. 枚舉類

    public enum ShopTypeEnum {
        TAOBAO("A","淘寶"),
        TMALL("B", "天貓"),
        TAOTE("C", "淘特");
        @Getter
        private String type;
        @Getter
        private String desc;
        ShopTypeEnum(String type, String des) {
            this.type = type;
            this.desc = des;
        }
    }
  2. 店鋪等級計算接口

    public interface ShopRankHandler {
        /**
        * 計算店鋪等級
        * @return 店鋪等級
        */
        String calculate();
    }
  3. 各類型店鋪等級計算策略實現類

    淘寶店

    public class TbShopRankHandleImpl implements ShopRankHandler{   
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }

    天貓店

    public class TmShopRankHandleImpl implements ShopRankHandler{
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }

    淘特店

    public class TtShopRankHandleImpl implements ShopRankHandler{
        @Override
        public String calculate() {
            // 具體計算邏輯
            return rank;
        }
    }
  4. 策略工廠類

    @Component
    public class ShopRankHandlerFactory {
        // 初始化策略beans
        private static final Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP = ImmutableMap.<String, ShopRankHandler>builder()
            .put(ShopTypeEnum.TAOBAO.getType(), new TbShopRankHandleImpl())
            .put(ShopTypeEnum.TMALL.getType(), new TmShopRankHandleImpl())
            .put(ShopTypeEnum.TAOTE.getType(), new TtShopRankHandleImpl())
            ;
        /**
         * 根據店鋪類型獲取對應的獲取店鋪卡片實現類
         *
         * @param shopType 店鋪類型
         * @return 店鋪類型對應的獲取店鋪卡片實現類
         */
        public ShopRankHandler getStrategy(String shopType) {
            return GET_SHOP_RANK_STRATEGY_MAP.get(shopType);
        }
    }
  5. 客戶端調用

    @Resource
    ShopRankHandlerFactory shopRankHandlerFactory;
    // 根據參數調用對應的算法計算店鋪等級
    public String acqurireShopRank(String shopType) {
        ShopRankHandler shopRankHandler = shopRankHandlerFactory.getStrategy(shopType);
        return Optional.ofNullable(shopRankHandler)
            .map(shopRankHandle -> shopRankHandle.calculate())
            .orElse(StringUtil.EMPTY_STRING);
    }

至此,當我們需要新增策略類時,需要做的改動如下:

  1. 新建策略類並實現策略接口

  2. 增加枚舉類型

  3. 工廠類中初始化時增加新的策略類

相比上一種方式,策略類與客戶端進行解耦,無需更改客戶端的代碼。

將客戶端與策略類進行解耦,客戶端只面向策略接口進行編程,對具體策略類的變化(更改、增刪)完全無感知,符合開閉原則。

需要引入額外的工廠類,使系統結構變得複雜。

當新加入策略類時,工廠類中初始化策略的部分仍然需要改動。

第二次迭代(利用 Spring 框架初始化策略 beans)

在枚舉 + 簡單工廠實現的方式中,利用簡單工廠將客戶端與具體的策略類實現進行了解耦,但工廠類中初始化策略 beans 的部分仍然與具體策略類存在耦合,爲了進一步解耦,我們可以利用 Spring 框架中的 InitializingBean 接口與 ApplicationContextAware 接口來實現策略 beans 的自動裝配。InitializingBean 接口中的 afterPropertiesSet() 方法在類的實例化過程當中執行,也就是說,當客戶端完成注入 ShopRankHandlerFactory 工廠類實例的時候,afterPropertiesSet() 也已經執行完成。因此我們可以通過重寫 afterPropertiesSet() 方法,在其中利用 getBeansOfType() 方法來獲取到策略接口的所有實現類,並存於 Map 容器之中,達到工廠類與具體的策略類解耦的目的。相比於上一種實現方式,需要改動的代碼如下:

  1. 店鋪等級計算接口

    public interface ShopRankHandler {
        /**
        * 獲取店鋪類型的方法,接口的實現類需要根據各自的枚舉類型來實現,後面就不貼出實現類的代碼
        * @return 店鋪等級
        */
        String getType();
        /**
        * 計算店鋪等級
        * @return 店鋪等級
        */
        String calculate();
    }
  2. 策略工廠類

    @Component
    public class ShopRankHandlerFactory implements InitializingBean, ApplicationContextAware {
        private ApplicationContext applicationContext;
        /**
         * 策略實例容器
         */
        private Map<String, ShopRankHandler> GET_SHOP_RANK_STRATEGY_MAP;
        /**
         * 根據店鋪類型獲取對應的獲取店鋪卡片實現類
         *
         * @param shopType 店鋪類型
         * @return 店鋪類型對應的獲取店鋪卡片實現類
         */
        public ShopRankHandler getStrategy(String shopType) {
            return GET_SHOP_RANK_STRATEGY_MAP.get(shopType);
        }
        @Override
        public void afterPropertiesSet() {
            Map<String, ShopRankHandler> beansOfType = applicationContext.getBeansOfType(ShopRankHandler.class);
            GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType)
                                .map(beansOfTypeMap -> beansOfTypeMap.values().stream()
                                        .filter(shopRankHandle -> StringUtils.isNotEmpty(shopRankHandle.getType()))
                                        .collect(Collectors.toMap(ShopRankHandler::getType, Function.identity())))
                                .orElse(new HashMap<>(8));
        }
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }

至此,當我們需要新增策略類時,需要做的改動如下:

  1. 新建策略類並實現策略接口

  2. 增加枚舉類型

相比於上一種方式,可以省略工廠類在初始化策略 beans 時要增加新的策略類這一步驟。

藉助 Spring 框架完成策略 beans 的自動裝配,使得策略工廠類與具體的策略類進一步解耦。

需要藉助 Spring 框架來完成,不過在 Spring 框架應用如此廣泛的今天,這個缺點可以忽略不計。

▐****最終迭代(利用泛型進一步提高策略工廠複用性)

經過上面兩次迭代以後,策略模式的實現已經變得非常方便,當需求發生改變的時候,我們再也不用手忙腳亂了,只需要關注新增或者變化的策略類就好,而不用侵入原有邏輯去開發。但是還有沒有改進的空間呢?

設想一下有一個新業務同樣需要策略模式來實現,如果爲其重新寫一個策略工廠類,整個策略工廠類中除了新的策略接口外,其他代碼均與之前的策略工廠相同,出現了大量重複代碼,這是我們所不能忍受的。爲了最大程度避免重複代碼的出現,我們可以使用泛型將策略工廠類中的策略接口參數化,使其變得更靈活,從而提高其的複用性。

理論存在,實踐開始!代碼示意如下:

  1. 定義泛型接口

    public interface GenericInterface<E> {
         E getType();
    }
  2. 定義策略接口繼承泛型接口

    public interface StrategyInterfaceA extends GenericInterface<String>{
        String handle();
    }
    public interface StrategyInterfaceB extends GenericInterface<Integer>{
        String handle();
    }
    public interface StrategyInterfaceC extends GenericInterface<Long>{
        String handle();
    }
  3. 實現泛型策略工廠

    public class HandlerFactory<E, T extends GenericInterface<E>> implements InitializingBean, ApplicationContextAware {
        private ApplicationContext applicationContext;
        /**
         * 泛型策略接口類型
         */
        private Class<T> strategyInterfaceType;
        /**
         * java泛型只存在於編譯期,無法通過例如T.class的方式在運行時獲取其類信息
         * 因此利用構造函數傳入具體的策略類型class對象爲getBeansOfType()方法
         * 提供參數
         *
         * @param strategyInterfaceType 要傳入的策略接口類型
         */
        public HandlerFactory(Class<T> strategyInterfaceType) {
            this.strategyInterfaceType = strategyInterfaceType;
        }
        /**
         * 策略實例容器
         */
        private Map<E, T> GET_SHOP_RANK_STRATEGY_MAP;
        /**
         * 根據不同參數類型獲取對應的接口實現類
         *
         * @param type 參數類型
         * @return 參數類型對應的接口實現類
         */
        public T getStrategy(E type) {
            return GET_SHOP_RANK_STRATEGY_MAP.get(type);
        }
        @Override
        public void afterPropertiesSet() {
            Map<String, T> beansOfType = applicationContext.getBeansOfType(strategyInterfaceType);
            System.out.println(beansOfType);
            GET_SHOP_RANK_STRATEGY_MAP = Optional.ofNullable(beansOfType)
                    .map(beansOfTypeMap -> beansOfTypeMap.values().stream()
                            .filter(strategy -> StringUtils.isNotEmpty(strategy.getType().toString()))
                            .collect(Collectors.toMap(strategy -> strategy.getType(), Function.identity())))
                    .orElse(new HashMap<>(8));
            System.out.println(GET_SHOP_RANK_STRATEGY_MAP);
        }
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }
  4. 有了上述泛型策略工廠類,當我們需要新建一個策略工廠類的時候,只需要利用其構造函數傳入相應的策略接口即可。生成 StrategyInterfaceA、StrategyInterfaceB 與 StrategyInterfaceC 接口的策略工廠如下:

    public class BeanConfig {
        @Bean
        public HandlerFactory<String, StrategyInterfaceA> strategyInterfaceAFactory(){
            return new HandlerFactory<>(StrategyInterfaceA.class);
        }
        @Bean
        public HandlerFactory<Integer, StrategyInterfaceB> strategyInterfaceBFactory(){
            return new HandlerFactory<>(StrategyInterfaceB.class);
        }
        @Bean
        public HandlerFactory<Long, StrategyInterfaceC> strategyInterfaceCFactory(){
            return new HandlerFactory<>(StrategyInterfaceC.class);
        }
    }

此時,若想新建一個策略工廠,則只需將策略接口作爲參數傳入泛型策略工廠即可,無需再寫重複的樣板代碼,策略工廠的複用性大大提高,也大大提高了我們的開發效率。

將策略接口類型參數化,策略工廠不受接口類型限制,成爲任意接口的策略工廠。

系統的抽象程度、複雜度變高,不利於直觀理解。

結束語

學習設計模式,關鍵是學習設計思想,不能簡單地生搬硬套,靈活正確地應用設計模式可以讓我們在開發中取得事半功倍的效果,但也不能爲了使用設計模式而過度設計,要合理平衡設計的複雜度和靈活性。

本文是對策略模式最佳實踐的一次探索,不一定是事實上的最佳實踐,歡迎大家指正與討論。

團隊介紹

我們是大聚划算技術團隊。
使命:讓貨品和心智運營變得高效且有確定性!
願景:與運營、產品合力,打造最具價格優惠心智的購物入口,最具爆發性的營銷矩陣。
職責:負責支持聚划算、百億補貼、天天特賣等業務。我們聚焦優惠和選購體驗,通過數智化驅動形成更有效率和確定性的貨品運營方法論,爲消費者提供精選和極致性價比的商品,爲商家提供更具爆發確定性的營銷方案。
這是一支極具業務 sense,又具備複雜業務系統架構和設計經驗的團隊。

作者 | 劉航(凌初)

編輯 | 橙子君

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Ovoy1CnN92xCYHbbO4H_DQ