策略模式-優(yōu)雅的改造短信業(yè)務模塊
點擊關注公眾號,Java干貨及時送達??

來源:www.jianshu.com/p/57a462ea9a6a
前言
最近在開發(fā)公司的短信模板功能,簡單的說,就是創(chuàng)建一些包含占位符的短信模板,在發(fā)送短信時將這些占位符使用特定值替換后再發(fā)出,例如短信模板中的公司名稱占位符是{companyName},在發(fā)送時,使用具體的公司名稱將{companyName}替換。
短信模板是一個獨立的服務,其他模塊在調(diào)用短信發(fā)送接口時,需要指定短信模板code以及要對占位符進行替換的占位符參數(shù);因為調(diào)用短信發(fā)送的業(yè)務場景比較多,如果某次調(diào)用傳入的占位符替換參數(shù)與對應短信模板占位符不匹配,會導致發(fā)出的短信還包含有未替換的占位符,影響到短信發(fā)送的有效性。因此,需要在發(fā)送短信時根據(jù)模板校驗傳入的占位符替換參數(shù)。
目前定下來的需求是短信模板與傳入的占位符替換參數(shù)必須完全對應才能發(fā)送短信,最簡單的方法就是在發(fā)送短信時加上判斷,如果不滿足條件則拒絕發(fā)送,但是考慮到后續(xù)的拓展性(例如按照業(yè)務場景設定不同的拒絕策略),這一個判斷過程最好是使用策略模式實現(xiàn)。
策略模式
在閻宏博士的《JAVA與模式》一書中開頭是這樣描述策略(Strategy)模式的:策略模式屬于對象的行為模式。其用意是針對一組算法,將每一個算法封裝到具有共同接口的獨立的類中,從而使得它們可以相互替換。策略模式使得算法可以在不影響到客戶端的情況下發(fā)生變化。
對于從事JAVA開發(fā)的CRUD工程師們而言,實際項目開發(fā)中更多都是寫業(yè)務邏輯,算法可以泛化成各種不同的業(yè)務場景,在同一個業(yè)務場景里,根據(jù)條件的不同需要提供多種不同的業(yè)務處理邏輯,這些業(yè)務處理邏輯的增加或減少是客戶端無需關注的。
業(yè)務代碼
本文主要是介紹策略模式,重點就只在于短信發(fā)送時拒絕策略邏輯的處理,不相關的代碼就不介紹了。

主要的接口有兩個 SmsTemplatePlaceHolderHandler 短信模板占位符處理器接口,SmsSendRejectStrategy短信發(fā)送拒絕策略接口,SmsTemplatePlaceHolderHandler有一個默認的實現(xiàn)類DefaultSmsTemplatePlaceHolderHandler,其關聯(lián)了一個SmsSendRejectStrategy實例,在發(fā)送短信時,具體的短信發(fā)送拒絕策略實現(xiàn)類將進行具體的發(fā)送拒絕邏輯的處理,如果允許發(fā)送,則由DefaultSmsTemplatePlaceHolderHandler將替換了占位符的短信模板內(nèi)容發(fā)出。
其中,DefaultSmsTemplatePlaceHolderHandler與SmsSendRejectStrategy的關系就是一個具體的策略模式的體現(xiàn),DefaultSmsTemplatePlaceHolderHandler無需關注拒絕發(fā)送的處理邏輯,調(diào)用SmsSendRejectStrategy實現(xià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?默認的短信模板占位符處理器
?*/
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?getParameterSet(SmsPlaceHolderParameter?parameter)? {
????????Map?parameterMap?=?getParameterMap(parameter);
????????return?parameterMap.keySet();
????}
????/**
?????*?獲取短信發(fā)送占位符替換參數(shù)Map(不包含value為null)
?????*
?????*?@param?parameter?填充占位符的參數(shù)
?????*?@return?Map
?????*/
????@NonNull
????default?Map?getParameterMap(SmsPlaceHolderParameter?parameter)? {
????????Map?parameterMap?=?JacksonUtils.toMap(parameter);
????????Map?filteredParameterMap?=?new?HashMap<>(4);
????????if?(parameterMap?!=?null)?{
????????????Set>?entrySet?=?parameterMap.entrySet();
????????????entrySet.forEach(stringObjectEntry?->?{
????????????????if?(stringObjectEntry.getValue()?!=?null)?{
????????????????????filteredParameterMap.put(stringObjectEntry.getKey(),?stringObjectEntry.getValue());
????????????????}
????????????});
????????}
????????return?filteredParameterMap;
????}
}
三種拒絕策略的實現(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ù)匹配了短信模板中的任意一個占位符key,就允許發(fā)送
?*/
@Slf4j
public?class?SmsSendAnyMatchStrategy?implements?SmsSendRejectStrategy?{
????@Override
????public?void?reject(SmsTemplateContext?templateContext,?SmsPlaceHolderParameter?parameter)?{
????????Set?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ā)送拒絕策略-完全匹配,只有當短信入?yún)⑴c短信模板占位符完全匹配時才允許發(fā)送
?*/
@Slf4j
public?class?SmsSendTotallyMatchStrategy?implements?SmsSendRejectStrategy?{
????@Override
????public?void?reject(SmsTemplateContext?templateContext,?SmsPlaceHolderParameter?parameter)?{
????????Set?parameterKeySet?=?getParameterSet(parameter);
????????if?(!parameterKeySet.containsAll(templateContext.getPlaceHolderKeySet()))?{
????????????log.error("短信占位符替換參數(shù)與短信模板不完全匹配,templateContent?=?{},parameter?=?{}",?templateContext.getTemplateContent(),?parameter);
????????????throw?new?CustomException("短信占位符替換參數(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ā)送服務
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("短信發(fā)送:?{}",?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);
????}
}
更改拒絕策略,發(fā)送短信時日志如下:
SmsSendAnyMatchStrategy
2021-09-04?14:34:36.261??INFO?5528?---?[???????????main]?com.cube.share.sms.service.SmsService????:?短信發(fā)送:?尊敬的用戶您好,{companyName}定于上海開展主題為{title}的營銷活動,活動時間{startTime}-{endTime},歡迎您的光臨!
可以看出,當拒絕策略為SmsSendAnyMatchStrategy時,只要占位符入?yún)⑴c短信模板中的占位符有一個匹配,就能夠發(fā)送成功
SmsSendTotallyMatchStrategy
占位符參數(shù)與模板占位符不完全匹配時發(fā)送失敗

2021-09-04?14:38:16.133?ERROR?3896?---?[???????????main]?c.c.s.s.s.SmsSendTotallyMatchStrategy????:?短信占位符替換參數(shù)與短信模板不完全匹配,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:?短信占位符替換參數(shù)與短信模板不完全匹配
????at?com.cube.share.sms.strategy.SmsSendTotallyMatchStrategy.reject(SmsSendTotallyMatchStrategy.java:22)
占位符參數(shù)與模板占位符完全匹配時發(fā)送成功

代碼示例:
https://gitee.com/li-cube/share/tree/master/sms
總結(jié)
業(yè)務邏輯說到底就是if-else,使用設計模式能夠使代碼更易維護、更易拓展,并且代碼的閱讀性更強;雖然不使用設計模式照樣能夠?qū)崿F(xiàn)業(yè)務,不過就是多套幾層if-else而已,但是人活著總歸要有點追求,只有做到不止于業(yè)務、不止于代碼,才能成為一個脫離低級CRUD的程序員。
4.?Typora 保姆級教程
最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關注公眾號并回復?Java?領取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號平臺更改了推送規(guī)則,如果不想錯過內(nèi)容,記得讀完點一下“在看”,加個“星標”,這樣每次新文章推送才會第一時間出現(xiàn)在你的訂閱列表里。
點“在看”支持小哈呀,謝謝啦??

