策略模式-短信模板业务场景

前言

最近在开发公司的短信模板功能,简单的说,就是创建一些包含占位符的短信模板,在发送短信时将这些占位符使用特定值替换后再发出,例如短信模板中的公司名称占位符是{companyName},在发送时,使用具体的公司名称将{companyName}替换。
短信模板是一个独立的服务,其他模块在调用短信发送接口时,需要指定短信模板code以及要对占位符进行替换的占位符参数;因为调用短信发送的业务场景比较多,如果某次调用传入的占位符替换参数与对应短信模板占位符不匹配,会导致发出的短信还包含有未替换的占位符,影响到短信发送的有效性。因此,需要在发送短信时根据模板校验传入的占位符替换参数。
目前定下来的需求是短信模板与传入的占位符替换参数必须完全对应才能发送短信,最简单的方法就是在发送短信时加上判断,如果不满足条件则拒绝发送,但是考虑到后续的拓展性(例如按照业务场景设定不同的拒绝策略),这一个判断过程最好是使用策略模式实现。

策略模式

在阎宏博士的《JAVA与模式》一书中开头是这样描述策略(Strategy)模式的:策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化
对于从事JAVA开发的CRUD工程师们而言,实际项目开发中更多都是写业务逻辑,算法可以泛化成各种不同的业务场景,在同一个业务场景里,根据条件的不同需要提供多种不同的业务处理逻辑,这些业务处理逻辑的增加或减少是客户端无需关注的。

业务代码

本文主要是介绍策略模式,重点就只在于短信发送时拒绝策略逻辑的处理,不相关的代码就不介绍了。

UML类图.png

主要的接口有两个 SmsTemplatePlaceHolderHandler 短信模板占位符处理器接口,SmsSendRejectStrategy短信发送拒绝策略接口,SmsTemplatePlaceHolderHandler有一个默认的实现类DefaultSmsTemplatePlaceHolderHandler,其关联了一个SmsSendRejectStrategy实例,在发送短信时,具体的短信发送拒绝策略实现类将进行具体的发送拒绝逻辑的处理,如果允许发送,则由DefaultSmsTemplatePlaceHolderHandler将替换了占位符的短信模板内容发出。
其中,DefaultSmsTemplatePlaceHolderHandlerSmsSendRejectStrategy的关系就是一个具体的策略模式的体现,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时,只要占位符入参与短信模板中的占位符有一个匹配,就能够发送成功

  • SmsSendTotallyMatchStrategy
    占位符参数与模板占位符不完全匹配时发送失败


    不完全匹配.png
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)

占位符参数与模板占位符完全匹配时发送成功

完全匹配.png

代码示例:https://gitee.com/li-cube/share/tree/master/sms

总结

业务逻辑说到底就是if-else,使用设计模式能够使代码更易维护、更易拓展,并且代码的阅读性更强;虽然不使用设计模式照样能够实现业务,不过就是多套几层if-else而已,但是人活着总归要有点追求,只有做到不止于业务、不止于代码,才能成为一个脱离低级CRUD的程序员。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容