工作中常见的设计模式-策略模式

  • 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即可,再也不需要自己手动去写不同的策略类了。