当前位置:网站首页>优雅的改造短信业务模块,策略模式走起!
优雅的改造短信业务模块,策略模式走起!
2022-06-29 22:36:00 【androidstarjack】
点击关注公众号,Java干货及时送达
来源:www.jianshu.com/p/57a462ea9a6a
前言
最近在开发公司的短信模板功能,简单的说,就是创建一些包含占位符的短信模板,在发送短信时将这些占位符使用特定值替换后再发出,例如短信模板中的公司名称占位符是{companyName},在发送时,使用具体的公司名称将{companyName}替换。
短信模板是一个独立的服务,其他模块在调用短信发送接口时,需要指定短信模板code以及要对占位符进行替换的占位符参数;因为调用短信发送的业务场景比较多,如果某次调用传入的占位符替换参数与对应短信模板占位符不匹配,会导致发出的短信还包含有未替换的占位符,影响到短信发送的有效性。因此,需要在发送短信时根据模板校验传入的占位符替换参数。
目前定下来的需求是短信模板与传入的占位符替换参数必须完全对应才能发送短信,最简单的方法就是在发送短信时加上判断,如果不满足条件则拒绝发送,但是考虑到后续的拓展性(例如按照业务场景设定不同的拒绝策略),这一个判断过程最好是使用策略模式实现。
策略模式
在阎宏博士的《JAVA与模式》一书中开头是这样描述策略(Strategy)模式的:策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
对于从事JAVA开发的CRUD工程师们而言,实际项目开发中更多都是写业务逻辑,算法可以泛化成各种不同的业务场景,在同一个业务场景里,根据条件的不同需要提供多种不同的业务处理逻辑,这些业务处理逻辑的增加或减少是客户端无需关注的。
业务代码
本文主要是介绍策略模式,重点就只在于短信发送时拒绝策略逻辑的处理,不相关的代码就不介绍了。

主要的接口有两个 SmsTemplatePlaceHolderHandler 短信模板占位符处理器接口,SmsSendRejectStrategy短信发送拒绝策略接口,SmsTemplatePlaceHolderHandler有一个默认的实现类DefaultSmsTemplatePlaceHolderHandler,其关联了一个SmsSendRejectStrategy实例,在发送短信时,具体的短信发送拒绝策略实现类将进行具体的发送拒绝逻辑的处理,如果允许发送,则由DefaultSmsTemplatePlaceHolderHandler将替换了占位符的短信模板内容发出。
其中,DefaultSmsTemplatePlaceHolderHandler与SmsSendRejectStrategy的关系就是一个具体的策略模式的体现,DefaultSmsTemplatePlaceHolderHandler无需关注拒绝发送的处理逻辑,调用SmsSendRejectStrategy实现类的实例进行处理即可。
DefaultSmsTemplatePlaceHolderHandler
package com.cube.share.sms.handler;
import com.cube.share.base.utils.JacksonUtils;
import com.cube.share.base.utils.PlaceHolderUtils;
import com.cube.share.sms.constant.SmsConstant;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
import com.cube.share.sms.strategy.SmsSendRejectStrategy;
import com.cube.share.sms.strategy.SmsTemplateContext;
/**
* @author cube.li
* @date 2021/9/4 12:27
* @description 默认的短信模板占位符处理器
*/
public class DefaultSmsTemplatePlaceHolderHandler implements SmsTemplatePlaceHolderHandler {
private SmsSendRejectStrategy rejectStrategy;
public DefaultSmsTemplatePlaceHolderHandler(SmsSendRejectStrategy rejectStrategy) {
this.rejectStrategy = rejectStrategy;
}
@Override
public String handle(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter) {
//发送拒绝策略
rejectStrategy.reject(templateContext, parameter);
return PlaceHolderUtils.replacePlaceHolder(templateContext.getTemplateContent(),
JacksonUtils.toMap(parameter),
SmsConstant.DEFAULT_PLACE_HOLDER_REGEX,
SmsConstant.DEFAULT_PLACE_HOLDER_KEY_REGEX);
}
}SmsSendRejectStrategy
package com.cube.share.sms.strategy;
import com.cube.share.base.utils.JacksonUtils;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
import org.springframework.lang.NonNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author cube.li
* @date 2021/9/4 9:49
* @description 短信发送的拒绝策略
*/
public interface SmsSendRejectStrategy {
/**
* 判断是否拒绝发送短信
*
* @param templateContext 短信模板上下文
* @param parameter 填充占位符的参数
*/
void reject(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter);
/**
* 获取短信发送占位符替换参数Set(不包含value为null)
*
* @param parameter 填充占位符的参数
* @return Set
*/
@NonNull
default Set<String> getParameterSet(SmsPlaceHolderParameter parameter) {
Map<String, Object> parameterMap = getParameterMap(parameter);
return parameterMap.keySet();
}
/**
* 获取短信发送占位符替换参数Map(不包含value为null)
*
* @param parameter 填充占位符的参数
* @return Map
*/
@NonNull
default Map<String, Object> getParameterMap(SmsPlaceHolderParameter parameter) {
Map<String, Object> parameterMap = JacksonUtils.toMap(parameter);
Map<String, Object> filteredParameterMap = new HashMap<>(4);
if (parameterMap != null) {
Set<Map.Entry<String, Object>> entrySet = parameterMap.entrySet();
entrySet.forEach(stringObjectEntry -> {
if (stringObjectEntry.getValue() != null) {
filteredParameterMap.put(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}
});
}
return filteredParameterMap;
}
}三种拒绝策略的实现类
package com.cube.share.sms.strategy;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
/**
* @author cube.li
* @date 2021/9/4 11:54
* @description 短信发送拒绝策略-忽略策略,无论短信发送入参与模板是否匹配,都允许发送
*/
public class SmsSendIgnoreStrategy implements SmsSendRejectStrategy {
@Override
public void reject(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter) {
//do nothing
}
}package com.cube.share.sms.strategy;
import com.cube.share.base.templates.CustomException;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.util.Set;
/**
* @author cube.li
* @date 2021/9/4 11:45
* @description SmsSendAnyMatchStrategy, 只要占位符参数匹配了短信模板中的任意一个占位符key,就允许发送
*/
@Slf4j
public class SmsSendAnyMatchStrategy implements SmsSendRejectStrategy {
@Override
public void reject(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter) {
Set<String> parameterKeySet = getParameterSet(parameter);
if (CollectionUtils.intersection(templateContext.getPlaceHolderKeySet(), parameterKeySet).size() <= 0) {
log.error("短信占位符替换参数与短信模板完全不匹配,templateContent = {},parameter = {}", templateContext.getTemplateContent(), parameter);
throw new CustomException("短信占位符替换参数与短信模板完全不匹配");
}
}
}package com.cube.share.sms.strategy;
import com.cube.share.base.templates.CustomException;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
import lombok.extern.slf4j.Slf4j;
import java.util.Set;
/**
* @author cube.li
* @date 2021/9/4 11:57
* @description 短信发送拒绝策略-完全匹配,只有当短信入参与短信模板占位符完全匹配时才允许发送
*/
@Slf4j
public class SmsSendTotallyMatchStrategy implements SmsSendRejectStrategy {
@Override
public void reject(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter) {
Set<String> parameterKeySet = getParameterSet(parameter);
if (!parameterKeySet.containsAll(templateContext.getPlaceHolderKeySet())) {
log.error("短信占位符替换参数与短信模板不完全匹配,templateContent = {},parameter = {}", templateContext.getTemplateContent(), parameter);
throw new CustomException("短信占位符替换参数与短信模板不完全匹配");
}
}
}拒绝策略实例的创建工厂
package com.cube.share.sms.factory;
import com.cube.share.sms.constant.SmsSendRejectStrategyEnum;
import com.cube.share.sms.strategy.SmsSendAnyMatchStrategy;
import com.cube.share.sms.strategy.SmsSendIgnoreStrategy;
import com.cube.share.sms.strategy.SmsSendRejectStrategy;
import com.cube.share.sms.strategy.SmsSendTotallyMatchStrategy;
/**
* @author cube.li
* @date 2021/9/4 12:49
* @description 拒绝策略工厂
*/
public class SmsSendRejectStrategyFactory {
private static final SmsSendIgnoreStrategy IGNORE_STRATEGY = new SmsSendIgnoreStrategy();
private static final SmsSendAnyMatchStrategy ANY_MATCH_STRATEGY = new SmsSendAnyMatchStrategy();
private static final SmsSendTotallyMatchStrategy TOTALLY_MATCH_STRATEGY = new SmsSendTotallyMatchStrategy();
public static SmsSendRejectStrategy getStrategy(SmsSendRejectStrategyEnum strategyEnum) {
switch (strategyEnum) {
case IGNORE:
return IGNORE_STRATEGY;
case ANY_MATCH:
return ANY_MATCH_STRATEGY;
case TOTALLY_MATCH:
return TOTALLY_MATCH_STRATEGY;
default:
throw new IllegalArgumentException("Illegal StrategyEnum Param");
}
}
}短信发送服务
package com.cube.share.sms.service;
import com.cube.share.base.templates.CustomException;
import com.cube.share.sms.config.SmsConfig;
import com.cube.share.sms.constant.SmsSendRejectStrategyEnum;
import com.cube.share.sms.factory.SmsSendRejectStrategyFactory;
import com.cube.share.sms.handler.DefaultSmsTemplatePlaceHolderHandler;
import com.cube.share.sms.handler.SmsTemplatePlaceHolderHandler;
import com.cube.share.sms.model.param.SmsSendParam;
import com.cube.share.sms.strategy.SmsTemplateContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author cube.li
* @date 2021/9/4 9:03
* @description 短信服务
*/
@Service
@Slf4j
public class SmsService {
@Resource
private SmsConfig smsConfig;
private SmsTemplatePlaceHolderHandler placeHolderHandler =
new DefaultSmsTemplatePlaceHolderHandler(SmsSendRejectStrategyFactory.getStrategy(SmsSendRejectStrategyEnum.ANY_MATCH));
public void send(SmsSendParam param) {
String templateContent = smsConfig.getTemplates().get(param.getTemplateCode());
if (templateContent == null) {
throw new CustomException("不正确的短信模板");
}
SmsTemplateContext templateContext = SmsTemplateContext.from(templateContent, param.getTemplateCode());
String sendContent = placeHolderHandler.handle(templateContext, param.getParameter());
log.info("短信发送: {}", sendContent);
}
}测试
短信模板在配置文件中
#短信
sms:
#模板
templates:
1: "尊敬的用户您好,{companyName}定于{address}开展主题为{title}的营销活动,活动时间{startTime}-{endTime},欢迎您的光临!"
2: "尊敬的用户您好,{address}开展主题为{title}的营销活动将于明天开始,欢迎您的光临!"单元测试类
package com.cube.share.sms.service;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
import com.cube.share.sms.model.param.SmsSendParam;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
/**
* @author cube.li
* @date 2021/9/4 12:00
* @description
*/
@SpringBootTest
class SmsServiceTest {
@Resource
SmsService smsService;
@Test
void send() {
SmsSendParam smsSendParam = new SmsSendParam();
smsSendParam.setTemplateCode(1);
SmsPlaceHolderParameter placeHolderParameter = new SmsPlaceHolderParameter();
placeHolderParameter.setAddress("上海");
smsSendParam.setParameter(placeHolderParameter);
smsService.send(smsSendParam);
}
}更改拒绝策略,发送短信时日志如下:
SmsSendAnyMatchStrategy
2021-09-04 14:34:36.261 INFO 5528 --- [ main] com.cube.share.sms.service.SmsService : 短信发送: 尊敬的用户您好,{companyName}定于上海开展主题为{title}的营销活动,活动时间{startTime}-{endTime},欢迎您的光临!可以看出,当拒绝策略为SmsSendAnyMatchStrategy时,只要占位符入参与短信模板中的占位符有一个匹配,就能够发送成功。PS:关于面试设计模式的必问面试题:面试必问:Mybatis中9种经典的设计模式!你知道几个?
SmsSendTotallyMatchStrategy
占位符参数与模板占位符不完全匹配时发送失败

2021-09-04 14:38:16.133 ERROR 3896 --- [ main] c.c.s.s.s.SmsSendTotallyMatchStrategy : 短信占位符替换参数与短信模板不完全匹配,templateContent = 尊敬的用户您好,{companyName}定于{address}开展主题为{title}的营销活动,活动时间{startTime}-{endTime},欢迎您的光临!,parameter = SmsPlaceHolderParameter(companyName=null, title=null, startTime=null, endTime=null, address=上海, url=null)
com.cube.share.base.templates.CustomException: 短信占位符替换参数与短信模板不完全匹配
at com.cube.share.sms.strategy.SmsSendTotallyMatchStrategy.reject(SmsSendTotallyMatchStrategy.java:22)占位符参数与模板占位符完全匹配时发送成功

代码示例:
https://gitee.com/li-cube/share/tree/master/sms
总结
业务逻辑说到底就是if-else,使用设计模式能够使代码更易维护、更易拓展,并且代码的阅读性更强;虽然不使用设计模式照样能够实现业务,不过就是多套几层if-else而已,但是人活着总归要有点追求,只有做到不止于业务、不止于代码,才能成为一个脱离低级CRUD的程序员。
今日好文推荐
GET 和 POST请求的本质区别是什么?看完觉得自己太无知了...
MyBatis批量插入数据你还在用foreach?你们的服务器没崩?

点个在看少个 bug
边栏推荐
- Hezhou air32f103cbt6 development board hands-on Report
- 5-2Web应用程序漏洞扫描
- 一键式文件共享软件Jirafeau
- Mysql database: read write separation
- Mysql database: partition
- 中国数据库崛起,阿里云李飞飞:中国云数据库多种主流技术创新已领先国外
- 5 - 1 Analyse de vulnérabilité du système
- SYSTEMd debugging
- Still stay up late every day and work overtime to make statements? In fact, you don't know how to make reports efficiently
- Design of Distributed Message Oriented Middleware
猜你喜欢

从零实现深度学习框架——RNN从理论到实战【实战】

Wireshark data analysis and forensics information pacapng

Steady! The best posture for thousands of microservices to access Zadig (helm chart)

字节云数据库未来方向的探索与实践

Unicom warehousing | all Unicom companies that need to sell their products need to enter the general warehouse first
Talk about auto in MySQL in detail_ What is the function of increment
![Realizing deep learning framework from zero -- LSTM from theory to practice [theory]](/img/ac/164140eff1a6518d49ce25599d9c7b.png)
Realizing deep learning framework from zero -- LSTM from theory to practice [theory]
![[php8+oracle11g+windows environment without tools] Intranet / no network /win10/php connecting to Oracle database instance](/img/72/214ee6d3842f393164cc93bb387926.png)
[php8+oracle11g+windows environment without tools] Intranet / no network /win10/php connecting to Oracle database instance

英语没学好到底能不能做coder,别再纠结了先学起来

Kr-gcn: an interpretable recommendation system based on knowledge aware reasoning
随机推荐
80-Redis详解
Day9 - user registration and login
0. grpc环境搭建
Problem solving metauniverse, multi communication scheme in online games
STM32 basic knowledge points
MySQL lock common knowledge points & summary of interview questions
leetcode 416. Partition Equal Subset Sum 分割等和子集(中等)
Portable 4K audio and video conference terminal all-in-one machine with 8x digital zoom
Golang code specification sorting
Hematemesis finishing: a rare map of architects!
分布式消息中间件设计
One click file sharing software jirafeau
C language tutorial – -6 loop statement
Digital tracking analysis of insurance services in the first quarter of 2022
合宙AIR32F103CBT6开发板上手报告
服务器快速搭建AList集成网盘网站【宝塔面板一键部署AList】
MySQL 锁常见知识点&面试题总结
模板函数与特化函数实现高效dynamicCast
英语没学好到底能不能做coder,别再纠结了先学起来
云原生爱好者周刊:炫酷的 Grafana 监控面板集合
