工作中常見的設計模式-策略模式

  • 2019 年 10 月 3 日
  • 筆記

前言

最近準備學習下之前項目中用到的設計模式,這裡程式碼都只展示核心業務程式碼,省略去大多不重要的程式碼。

程式碼大多是之前一起工作的小夥伴coding出來的,我這裡做一個學習和總結,我相信技術能力的提高都是先從模仿開始的,學習別人的程式碼及設計思想也是一種提升的方式。

 

後續還會有觀察者模式、責任鏈模式的部落格產出,都是工作中正式運用到的場景輸出,希望對看文章的你也有啟發和幫助。

一、業務需求

 
我之前做過在線問診的需求,業務複雜,很多節點需要出發消息推送,比如用戶下單 需要給醫生推送簡訊和push、醫生接診 需要給用戶發送簡訊、push、微信等。產品說後期會有很多不同的節點觸發消息發送。
 
這裡就開始抽象需求,首先是發送消息,很多消息是同樣的策略,只是組裝的數據是動態拼接的,所以抽象出:buildSms()、buildPush()、buildWechat() 等構造消息體的方法,對於拼接欄位相同的都採用同一策略,列入消息A、B需要通過醫生id拼接消息,消息C、D需要通過用戶id拼接消息,那麼A、B就採用同一策略,C、D採用另一策略。
 
流程圖大致如下:
 
各個業務系統 根據策略構造自己的消息體,然後通過kafka發送個底層服務,進行消息統一推送。
 
 

二、策略模式

 
策略模式(Strategy Pattern)指的是對象具備某個行為,但是在不同的場景中,該行為有不同的實現演算法。比如一個人的交稅比率與他的工資有關,不同的工資水平對應不同的稅率。
策略模式 使用的就是面向對象的繼承和多態機制,從而實現同一行為在不同場景下具備不同實現。
策略模式本質:分離演算法,選擇實現
 
主要解決在有多重演算法相似的情況下,使用if…else 或者switch…case所帶來的的複雜性和臃腫性。
 
 
程式碼示例:
 1 class Client {   2     public static void main(String[] args) {   3         ICalculator calculator = new Add();   4         Context context = new Context(calculator);   5         int result = context.calc(1,2);   6         System.out.println(result);   7     }   8   9  10     interface ICalculator {  11         int calc(int a, int b);  12     }  13  14  15     static class Add implements ICalculator {  16         @Override  17         public int calc(int a, int b) {  18             return a + b;  19         }  20     }  21  22  23     static class Sub implements ICalculator {  24         @Override  25         public int calc(int a, int b) {  26             return a - b;  27         }  28     }  29  30  31     static class Multi implements ICalculator {  32         @Override  33         public int calc(int a, int b) {  34             return a * b;  35         }  36     }  37  38  39     static class Divide implements ICalculator {  40         @Override  41         public int calc(int a, int b) {  42             return a / b;  43         }  44     }  45  46  47     static class Context {  48         private ICalculator mCalculator;  49  50  51         public Context(ICalculator calculator) {  52             this.mCalculator = calculator;  53         }  54  55  56         public int calc(int a, int b) {  57             return this.mCalculator.calc(a, b);  58         }  59     }}

 

三、工作中實際程式碼演示

 
為了程式碼簡潔和易懂,這裡用的都是核心程式碼片段,主要看策略使用的方式以及思想即可。
 

1、消息枚舉類,這裡因為消息出發節點眾多,所以每一個節點都會對應一個枚舉類,枚舉中包含簡訊、push、微信、私信等內容。

 1 @Getter   2 public enum MsgCollectEnum {   3   4     /**   5      * 枚舉入口:用戶首次提問 給醫生 文案內容(醫生id拼連接)   6      */   7     FIRST_QUESTION_CONTENT(2101, 1, MsgSmsEnum.SMS_FIRST_QUESTION_CONTENT, MsgPushEnum.PUSH_FIRST_QUESTION_CONTENT, MsgWechatEnum.WECHAT_FIRST_QUESTION_CONTENT);   8   9  10    /**  11      * 簡訊文案:用戶首次提問 給醫生 文案內容  12      */  13     SMS_FIRST_QUESTION_CONTENT(STTurnLinkEnum.DOCTOR_QUESTION_SETTING_PAGE.getStoapp(), "您好,有一位用戶向您發起諮詢,請確認接單,趕快進入APP查看吧!{0}");  14  15  16     /**  17      * Push文案:用戶首次提問 給醫生 文案內容  18      */  19     PUSH_FIRST_QUESTION_CONTENT(STTurnLinkEnum.DOCTOR_QUESTION_SETTING_PAGE.getStoapp(), STPushAudioEnum.PAY_SUCCESS.getType(), "您好, 有一位用戶向您發起了諮詢服務");  20  21  22     ......  23 }

 

 

2,消息節點觸發程式碼

這裡是構造上下文MsgContext,主要策略分發的邏輯在最後一行,這裡也會作為重點來講解
 

1 MsgContext msgContext = new MsgContext();  2 msgContext.setDoctorId(questionDO.getDoctorId());  3 msgContext.setReceiveUid(questionDO.getDrUid());  4 msgContext.setMsgType(MsgCollectEnum.FIRST_QUESTION_CONTENT.getType());  5 this.stContextStrategyFactory.doStrategy(String.valueOf(msgContext.getMsgType()), QuestionMsgStrategy.class).handleSeniority(msgContext);

 

3,策略分發

首先,通過QuestionMsgStrategy.class 找到對應所有的beanMap,然後通過自定義註解找到所有對應策略類,最後通過msgType找到指定的實現類。接著我們看下策略實現類
 
 1 @Slf4j   2 public class STContextStrategyFactory {   3     public <O extends STIContext, T extends STIContextStrategy<O>> STIContextStrategy<O> doStrategy(String type, Class<T> clazz) {   4         Map<String, T> beanMap = STSpringBeanUtils.getBeanMap(clazz);   5         if (MapUtils.isEmpty(beanMap)) {   6             log.error("獲取class:{} 為空", clazz.getName());   7         }   8         try {   9             for (Map.Entry<String, T> entry : beanMap.entrySet()) {  10                 Object real = STAopTargetUtils.getTarget(entry.getValue());  11                 STStrategyAnnotation annotation = real.getClass().getAnnotation(STStrategyAnnotation.class);  12                 List<String> keySet = Splitter.on("-").omitEmptyStrings().trimResults().splitToList(annotation.type());  13                 if (keySet.contains(type)) {  14                     return entry.getValue();  15                 }  16             }  17         } catch (Exception e) {  18             log.error("獲取目標代理對象失敗:{}", e);  19         }  20         log.error("strategy type = {} handle is null", type);  21         return null;  22     }  23 }

 
 

4,策略實現類

通過自定義註解,然後解析msgType值找到指定策略類,通過不同的策略類構造的msg 發送給kafka。
 
 1 @Component   2 @STStrategyAnnotation(type = "2101-2104-2113-2016", description = "發給醫生,無其他附屬資訊")   3 public class QuestionMsgSimpleToDoctorStrategyImpl extends AbstractQuestionSendMsgStrategy {   4   5   6     @Autowired   7     private RemoteMsgService remoteMsgService;   8     @Autowired   9     private QuestionDetailService questionDetailService;  10  11  12     @Override  13     public StarSmsIn buildSmsIn(MsgContext context) {  14         // do something  15     }  16  17  18     @Override  19     public StarPushIn buildPushIn(MsgContext context) {  20         // do something  21     }  22  23  24     ......  25  26  27 }  28  29  30 @Slf4j  31 public abstract class AbstractQuestionSendMsgStrategy implements QuestionMsgStrategy {  32     /**  33      * 構建簡訊消息  34      *  35      * @param context  36      * @return  37      */  38     public abstract StarSmsIn buildSmsIn(MsgContext context);  39  40  41     /**  42      * 構建push消息  43      *  44      * @param context  45      * @return  46      */  47     public abstract StarPushIn buildPushIn(MsgContext context);  48  49  50     /**  51      * 構建微信公眾號  52      *  53      * @param context  54      * @return  55      */  56     public abstract StarWeChatIn buildWeChatIn(MsgContext context);  57  58  59      @Override  60     public STResultInfo handleSeniority(MsgContext msgContext) {  61         // buildMsg and send kafka  62     }  63 }

 
 

四,策略模式缺點

整個消息系統的設計起初是基於此策略模式來實現的,但是在後續迭代開發中會發現越來越不好維護,主要缺點如下:
a、接入消息推送的研發同學需要了解每個策略類,對於相同的策略進行復用
b、節點越來越多,策略類也越來越多,系統不易維護
c、觸發節點枚舉類散落在各個業務系統中,經常會有相同的節點而不同的msgType
 
針對於上述的缺點,又重構了一把消息系統,此次是完全採用節點配置化方案,提供一個可視化頁面進行配置,將要構造的消息體通過配置寫入到資料庫中,程式碼中通過不同的佔位符進行數據動態替換。
這裡就不再展示新版系統的程式碼了,重構後 接入方只需要構造msgContext即可,再也不需要自己手動去寫不同的策略類了。