前言
最近准备学习下之前项目中用到的设计模式,这里代码都只展示核心业务代码,省略去大多不重要的代码。
代码大多是之前一起工作的小伙伴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 
 @Override25 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, Textends 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  
@Override19 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 
context54  * @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即可,再也不需要自己手动去写不同的策略类了。 
热门工具 换一换