前言
最近在開發(fā)公司的短信模板功能仇让,簡(jiǎn)單的說,就是創(chuàng)建一些包含占位符的短信模板躺翻,在發(fā)送短信時(shí)將這些占位符使用特定值替換后再發(fā)出丧叽,例如短信模板中的公司名稱占位符是{companyName},在發(fā)送時(shí)公你,使用具體的公司名稱將{companyName}替換踊淳。
短信模板是一個(gè)獨(dú)立的服務(wù),其他模塊在調(diào)用短信發(fā)送接口時(shí)省店,需要指定短信模板code以及要對(duì)占位符進(jìn)行替換的占位符參數(shù)嚣崭;因?yàn)檎{(diào)用短信發(fā)送的業(yè)務(wù)場(chǎng)景比較多笨触,如果某次調(diào)用傳入的占位符替換參數(shù)與對(duì)應(yīng)短信模板占位符不匹配懦傍,會(huì)導(dǎo)致發(fā)出的短信還包含有未替換的占位符辜王,影響到短信發(fā)送的有效性绝页。因此忘朝,需要在發(fā)送短信時(shí)根據(jù)模板校驗(yàn)傳入的占位符替換參數(shù)减细。
目前定下來的需求是短信模板與傳入的占位符替換參數(shù)必須完全對(duì)應(yīng)才能發(fā)送短信儡嘶,最簡(jiǎn)單的方法就是在發(fā)送短信時(shí)加上判斷御蒲,如果不滿足條件則拒絕發(fā)送纵顾,但是考慮到后續(xù)的拓展性(例如按照業(yè)務(wù)場(chǎng)景設(shè)定不同的拒絕策略)敬鬓,這一個(gè)判斷過程最好是使用策略模式實(shí)現(xiàn)串慰。
策略模式
在閻宏博士的《JAVA與模式》一書中開頭是這樣描述策略(Strategy)模式的:策略模式屬于對(duì)象的行為模式偏塞。其用意是針對(duì)一組算法,將每一個(gè)算法封裝到具有共同接口的獨(dú)立的類中邦鲫,從而使得它們可以相互替換灸叼。策略模式使得算法可以在不影響到客戶端的情況下發(fā)生變化。
對(duì)于從事JAVA開發(fā)的CRUD工程師們而言庆捺,實(shí)際項(xiàng)目開發(fā)中更多都是寫業(yè)務(wù)邏輯古今,算法可以泛化成各種不同的業(yè)務(wù)場(chǎng)景,在同一個(gè)業(yè)務(wù)場(chǎng)景里滔以,根據(jù)條件的不同需要提供多種不同的業(yè)務(wù)處理邏輯捉腥,這些業(yè)務(wù)處理邏輯的增加或減少是客戶端無需關(guān)注的。
業(yè)務(wù)代碼
本文主要是介紹策略模式你画,重點(diǎn)就只在于短信發(fā)送時(shí)拒絕策略邏輯的處理抵碟,不相關(guān)的代碼就不介紹了桃漾。
主要的接口有兩個(gè)
SmsTemplatePlaceHolderHandler
短信模板占位符處理器接口,SmsSendRejectStrategy
短信發(fā)送拒絕策略接口立磁,SmsTemplatePlaceHolderHandler
有一個(gè)默認(rèn)的實(shí)現(xiàn)類DefaultSmsTemplatePlaceHolderHandler
呈队,其關(guān)聯(lián)了一個(gè)SmsSendRejectStrategy
實(shí)例,在發(fā)送短信時(shí)唱歧,具體的短信發(fā)送拒絕策略實(shí)現(xiàn)類將進(jìn)行具體的發(fā)送拒絕邏輯的處理宪摧,如果允許發(fā)送,則由DefaultSmsTemplatePlaceHolderHandler
將替換了占位符的短信模板內(nèi)容發(fā)出颅崩。其中几于,
DefaultSmsTemplatePlaceHolderHandler
與SmsSendRejectStrategy
的關(guān)系就是一個(gè)具體的策略模式的體現(xiàn),DefaultSmsTemplatePlaceHolderHandler
無需關(guān)注拒絕發(fā)送的處理邏輯沿后,調(diào)用SmsSendRejectStrategy
實(shí)現(xiàn)類的實(shí)例進(jìn)行處理即可沿彭。
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 默認(rèn)的短信模板占位符處理器
*/
public class DefaultSmsTemplatePlaceHolderHandler implements SmsTemplatePlaceHolderHandler {
private SmsSendRejectStrategy rejectStrategy;
public DefaultSmsTemplatePlaceHolderHandler(SmsSendRejectStrategy rejectStrategy) {
this.rejectStrategy = rejectStrategy;
}
@Override
public String handle(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter) {
//發(fā)送拒絕策略
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 短信發(fā)送的拒絕策略
*/
public interface SmsSendRejectStrategy {
/**
* 判斷是否拒絕發(fā)送短信
*
* @param templateContext 短信模板上下文
* @param parameter 填充占位符的參數(shù)
*/
void reject(SmsTemplateContext templateContext, SmsPlaceHolderParameter parameter);
/**
* 獲取短信發(fā)送占位符替換參數(shù)Set(不包含value為null)
*
* @param parameter 填充占位符的參數(shù)
* @return Set
*/
@NonNull
default Set<String> getParameterSet(SmsPlaceHolderParameter parameter) {
Map<String, Object> parameterMap = getParameterMap(parameter);
return parameterMap.keySet();
}
/**
* 獲取短信發(fā)送占位符替換參數(shù)Map(不包含value為null)
*
* @param parameter 填充占位符的參數(shù)
* @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;
}
}
三種拒絕策略的實(shí)現(xiàn)類
package com.cube.share.sms.strategy;
import com.cube.share.sms.model.param.SmsPlaceHolderParameter;
/**
* @author cube.li
* @date 2021/9/4 11:54
* @description 短信發(fā)送拒絕策略-忽略策略,無論短信發(fā)送入?yún)⑴c模板是否匹配,都允許發(fā)送
*/
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, 只要占位符參數(shù)匹配了短信模板中的任意一個(gè)占位符key,就允許發(fā)送
*/
@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("短信占位符替換參數(shù)與短信模板完全不匹配,templateContent = {},parameter = {}", templateContext.getTemplateContent(), parameter);
throw new CustomException("短信占位符替換參數(shù)與短信模板完全不匹配");
}
}
}
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 短信發(fā)送拒絕策略-完全匹配,只有當(dāng)短信入?yún)⑴c短信模板占位符完全匹配時(shí)才允許發(fā)送
*/
@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("短信占位符替換參數(shù)與短信模板不完全匹配,templateContent = {},parameter = {}", templateContext.getTemplateContent(), parameter);
throw new CustomException("短信占位符替換參數(shù)與短信模板不完全匹配");
}
}
}
拒絕策略實(shí)例的創(chuàng)建工廠
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");
}
}
}
短信發(fā)送服務(wù)
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 短信服務(wù)
*/
@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("短信發(fā)送: {}", sendContent);
}
}
測(cè)試
短信模板在配置文件中
#短信
sms:
#模板
templates:
1: "尊敬的用戶您好,{companyName}定于{address}開展主題為{title}的營銷活動(dòng)尖滚,活動(dòng)時(shí)間{startTime}-{endTime}喉刘,歡迎您的光臨!"
2: "尊敬的用戶您好,{address}開展主題為{title}的營銷活動(dòng)將于明天開始漆弄,歡迎您的光臨!"
單元測(cè)試類
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);
}
}
更改拒絕策略睦裳,發(fā)送短信時(shí)日志如下:
- SmsSendAnyMatchStrategy
2021-09-04 14:34:36.261 INFO 5528 --- [ main] com.cube.share.sms.service.SmsService : 短信發(fā)送: 尊敬的用戶您好,{companyName}定于上海開展主題為{title}的營銷活動(dòng)撼唾,活動(dòng)時(shí)間{startTime}-{endTime}廉邑,歡迎您的光臨!
可以看出,當(dāng)拒絕策略為SmsSendAnyMatchStrategy
時(shí)倒谷,只要占位符入?yún)⑴c短信模板中的占位符有一個(gè)匹配蛛蒙,就能夠發(fā)送成功
-
SmsSendTotallyMatchStrategy
占位符參數(shù)與模板占位符不完全匹配時(shí)發(fā)送失敗
2021-09-04 14:38:16.133 ERROR 3896 --- [ main] c.c.s.s.s.SmsSendTotallyMatchStrategy : 短信占位符替換參數(shù)與短信模板不完全匹配,templateContent = 尊敬的用戶您好,{companyName}定于{address}開展主題為{title}的營銷活動(dòng)渤愁,活動(dòng)時(shí)間{startTime}-{endTime}牵祟,歡迎您的光臨!,parameter = SmsPlaceHolderParameter(companyName=null, title=null, startTime=null, endTime=null, address=上海, url=null)
com.cube.share.base.templates.CustomException: 短信占位符替換參數(shù)與短信模板不完全匹配
at com.cube.share.sms.strategy.SmsSendTotallyMatchStrategy.reject(SmsSendTotallyMatchStrategy.java:22)
占位符參數(shù)與模板占位符完全匹配時(shí)發(fā)送成功
代碼示例:https://gitee.com/li-cube/share/tree/master/sms
總結(jié)
業(yè)務(wù)邏輯說到底就是if-else,使用設(shè)計(jì)模式能夠使代碼更易維護(hù)抖格、更易拓展诺苹,并且代碼的閱讀性更強(qiáng);雖然不使用設(shè)計(jì)模式照樣能夠?qū)崿F(xiàn)業(yè)務(wù)他挎,不過就是多套幾層if-else而已筝尾,但是人活著總歸要有點(diǎn)追求,只有做到不止于業(yè)務(wù)办桨、不止于代碼筹淫,才能成為一個(gè)脫離低級(jí)CRUD的程序員。