当前位置:网站首页>优雅的改造短信业务模块,策略模式走起!
优雅的改造短信业务模块,策略模式走起!
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
边栏推荐
- 80-Redis详解
- Problem solving metauniverse, multi communication scheme in online games
- 分布式消息中间件设计
- 0. grpc environment setup
- Free PDF to word software sharing, these software must know!
- 5-1 system vulnerability scanning
- 直播平台开发,进入可视区域执行动画、动效、添加样式类名
- Processing of error b6267342 reported by AIX small machine in production environment
- Live broadcast platform development, enter the visual area to execute animation, dynamic effects and add style class names
- Unity Pac Man games, maze implementation
猜你喜欢

One click file sharing software jirafeau

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

深入解析kubernetes controller-runtime

剑指 Offer 38. 字符串的排列

VS无法定位程序输入点于动态链接库

Wireshark data analysis and forensics information pacapng

Qdomdocument and qdomnode are used in QT to read XML

Nacos-配置中心基本使用

What if MySQL fails to store emoticons

Vs2013 how to make the program run on other computers
随机推荐
The client can connect to remote MySQL
低代码、端到端,一小时构建IoT示例场景,声网发布灵隼物联网云平台
Static keyword continuation, inheritance, rewrite, polymorphism
众昂矿业:萤石助力氟产业锂电建设发展
5-2Web应用程序漏洞扫描
js函数相关的复习
STM32基础知识点
还天天熬夜加班做报表?其实你根本不懂如何高效做报表
[从零开始学习FPGA编程-51]:高阶篇 - 基于IP核的FPGA开发- 什么是FPGA IP核(软核、固核、硬核)与学习方法
Day9 - user registration and login
uniapp复制内容到剪贴板
模板函数与特化函数实现高效dynamicCast
Kr-gcn: an interpretable recommendation system based on knowledge aware reasoning
error: C2665: “QMessageBox::critical”: 4 个重载中没有一个可以转换所有参数类型
111.简易聊天室14:聊天室客户端
Gnawing down the big bone - sorting (I)
Evolution from stand-alone to distributed database storage system
VS无法定位程序输入点于动态链接库
Golang code specification sorting
VS2013如何让编写的程序在其它电脑上面也能运行
