0、前言
最近在spring里面扒设计模式:
从Spring中扒设计模式之策略适配器
从Spring中扒设计模式之模板方法
要是仅仅只是给大家看个热闹,那肯定是不够。所以我这里给大家带来了重构实战,给大家看看我在实际工作中是如何用扒下来的设计模式来重构代码的。
ps:重复使用的代码我就不贴了,文末会附上项目源码仓库地址
1、以前的业务实现方式
我这里用的是一个开源项目,java的微信开发工具包,功能很全面。 去到仓库地址 https://gitee.com/binary/weixin-java-tools
<!--公众号-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.6.0</version>
</dependency>
这里简单模拟服务类,真实使用的话还需要配置公众号
/**
* @author YearOfTheRain
* @create 2024-06-17 14:56
*/
@Configuration
public class Config {
@Bean
public WxMpService wxMpService() {
WxMpService service = new WxMpServiceImpl();
return service;
}
}
代码目录
模板配置
定义一个TmpBase,存放通用的字段
@Data
@EqualsAndHashCode(callSuper = false)
public class TmpBase implements Serializable {
private static final long serialVersionUID = 1L;
//userId,openid 填写其中一个
/**
* 接收者userId
*/
private String userId;
/**
* 用户点击消息跳转链接
*/
private String url;
/**
* 接收者openid
*/
private String openid;
public String getUrl() {
if (ObjectUtil.isNull(url)) {
return "";
}
return url;
}
}
定义两个具体的模板
/**
* 购买成功消息推送模板
*
* 模板示例
* 订单支付成功通知
*
* 订单号 G202205230000010
* 商品名称 矿泉水
* 支付方式 微信支付
* 支付金额 200.00
* 下单时间 2022年11月11日 12:12
*
* 点击查看订单
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BuySuccessTmp extends TmpBase implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 购买时间
*/
private Date buyTime;
/**
* 商品名称
*/
private String goodsName;
/**
* 支付金额
*/
private BigDecimal paymentAmount;
/**
* 付款方式(零售币/积分/微信支付。。)
*/
private String paymentMethod;
/** 订单号 */
private String orderNo;
}
@Data
@EqualsAndHashCode(callSuper = false)
public class BeginGroupTmp extends TmpBase implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品价格
*/
private BigDecimal goodsPrice;
/**
* 成团人数
*/
private int groupAmount;
/**
* 组团时间限制 以小时为单位
*/
private int groupTime;
}
消息发送服务
定义接口
/**
* 微信模板消息发送方法
* @author YearOfTheRain
* @create 2024-06-17 14:36
*/
public interface WxTemplateMsgService {
/**
* 购买成功通知
*/
boolean buySuccess(BuySuccessTmp buySuccessTmp);
/**
* 加入拼团成功通知
*/
boolean joinGroup(BeginGroupTmp beginGroupTmp);
}
实现具体的方法
/**
* 实现方法
* @author YearOfTheRain
* @create 2024-06-17 14:38
*/
@Service("oldWxTemplateMsgServiceImpl")
@Slf4j
public class WxTemplateMsgServiceImpl implements WxTemplateMsgService {
@Autowired
private WxMpService wxmpservice;
@Override
public boolean buySuccess(BuySuccessTmp tmp) {
if (ifOpen()) {
return false;
}
String openid;
if (!StrUtil.isBlank(tmp.getOpenid())) {
openid = tmp.getOpenid();
} else {
openid = checkOpenId(tmp.getUserId());
}
if (!StrUtil.isBlank(openid)) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
// 接收者openid
.toUser(openid)
// 模板id 模板标题:支付成功通知
.templateId("buySuccess")
// 模板跳转链接
.url(tmp.getUrl())
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "您已成功下单", "#888888"))
.addData(new WxMpTemplateData("time4", tmp.getGoodsName(), "#AAAAAA"))
.addData(new WxMpTemplateData("amount5", tmp.getPaymentAmount().setScale(2, RoundingMode.DOWN).toPlainString() + "零售币", "#AAAAAA"))
.addData(new WxMpTemplateData("thing3", tmp.getPaymentMethod() + "人", "#AAAAAA"))
.addData(new WxMpTemplateData("character_string2", tmp.getBuyTime() + "小时", "#AAAAAA"))
.addData(new WxMpTemplateData("remark", "详情请点击这里,祝生活愉快", "#0000FF"));
return judge(templateMessage);
}
return false;
}
@Override
public boolean joinGroup(BeginGroupTmp tmp) {
if (ifOpen()) {
return false;
}
String openid;
if (!StrUtil.isBlank(tmp.getOpenid())) {
openid = tmp.getOpenid();
} else {
openid = checkOpenId(tmp.getUserId());
}
if (!StrUtil.isBlank(openid)) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
// 接收者openid
.toUser(openid)
// 模板id 模板标题:开团成功通知
.templateId("begingroupSuccess")
// 模板跳转链接
.url(tmp.getUrl())
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "您已成功加入一个拼团", "#888888"))
.addData(new WxMpTemplateData("keyword1", tmp.getGoodsName(), "#AAAAAA"))
.addData(new WxMpTemplateData("keyword2", tmp.getGoodsPrice().setScale(2, RoundingMode.DOWN).toPlainString() + "零售币", "#AAAAAA"))
.addData(new WxMpTemplateData("keyword3", tmp.getGroupAmount() + "人", "#AAAAAA"))
.addData(new WxMpTemplateData("keyword4", tmp.getGroupTime() + "小时", "#AAAAAA"))
.addData(new WxMpTemplateData("remark", "详情请点击这里,祝生活愉快", "#0000FF"));
return judge(templateMessage);
}
return false;
}
//通过userId拿openid并判断是否关注公众号
public String checkOpenId(String UserId) {
// 获取openId
// 检查openId
return "openId";
}
//判断动态参数的微信模板消息开关是否开启
private boolean ifOpen() {
// 这里可从数据库读取消息开关配置
return false;
}
//发送模板消息并返回是否成功
public boolean judge(WxMpTemplateMessage templateMessage) {
log.info("执行发送模板消息方法");
String msgId = null;
try {
// 发送模板消息
msgId = wxmpservice.getTemplateMsgService().sendTemplateMsg(templateMessage);
} catch (WxErrorException e) {
log.error("发送公众号模板消息出现异常,消息发送参数为[{}]", templateMessage, e);
// 记录异常信息 43004 是用户未关注、未关注消息不推送,只打印日志
if (e.getError().getErrorCode() != 43004) {
ErrorInfoUtil.sendErrorMessageTo(e, "公众号消息");
}
}
return msgId != null;
}
}
大家可以看,两个消息发送接口流程一致,除了传入的模板不同。我这里只写了两个模板,在生产中我们有20+模板,大家可以想象得到这个类有多恐怖了吧。
2、使用模板方法重构
这里用公众号发送模板消息的功能进行示例,公众号发送模板消息,基础逻辑都一样,仅仅是调用不同的模板和配置不同的参数
定义流程骨架
这里定义了一个抽象的模板发送类,定义消息发送的主流程,将模板组装方法 buildTemplate() 留个具体的子类实现。
/**
* 使用模板方法重构,定义执行逻辑
* @param <T>
*/
@Slf4j
abstract class AbstractTemplateMessageHandler<T extends TmpBase> {
@Autowired
private WxMpService wxmpservice;
/**
* 模板消息发送
* @param temp
* @return
*/
public boolean send(T temp) {
if (ifOpen()){
return false;
}
String openid = this.getOpenId(temp);
if (StrUtil.isBlank(openid)) {
return false;
}
// 设置最终发送消息的openID
temp.setOpenid(openid);
log.info("最终发送消息的openID为[{}]", openid);
WxMpTemplateMessage templateMessage = buildTemplate(temp);
return judge(templateMessage);
}
//发送模板消息并返回是否成功
private boolean judge(WxMpTemplateMessage templateMessage) {
log.info("执行发送模板消息方法");
String msgId = null;
try {
// 发送模板消息
msgId = wxmpservice.getTemplateMsgService().sendTemplateMsg(templateMessage);
} catch (WxErrorException e) {
log.error("发送公众号模板消息出现异常,消息发送参数为[{}]", templateMessage, e);
// 记录异常信息 43004 是用户未关注、未关注消息不推送,只打印日志
if (e.getError().getErrorCode() != 43004) {
ErrorInfoUtil.sendErrorMessageTo(e, "公众号消息");
}
}
return msgId != null;
}
//判断动态参数的微信模板消息开关是否开启
private boolean ifOpen() {
// 这里可从数据库读取消息开关配置
return false;
}
/**
* 组装待发送的模板消息
* @param temp
* @return
*/
abstract WxMpTemplateMessage buildTemplate(T temp);
/**
* 获取 openId
* @param tmpBase openId
* @return
*/
private String getOpenId(TmpBase tmpBase) {
if (StrUtil.isNotBlank(tmpBase.getOpenid())) {
return tmpBase.getOpenid();
}
// 根据用户ID查询openID
// 获取openId
// 检查openId
return "openId";
}
}
实现骨架个性化方法
/**
* 实现方法
* @author YearOfTheRain
* @create 2024-06-17 14:38
*/
@Service("new1WxTemplateMsgServiceImpl")
@Slf4j
public class WxTemplateMsgServiceImpl implements WxTemplateMsgService {
@Override
public boolean buySuccess(BuySuccessTmp tmp) {
AbstractTemplateMessageHandler<BuySuccessTmp> messageHandler = new AbstractTemplateMessageHandler<BuySuccessTmp>() {
@Override
WxMpTemplateMessage buildTemplate(BuySuccessTmp temp) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
// 接收者openid
.toUser(temp.getOpenid())
// 模板id 模板标题:支付成功通知
.templateId("buySuccess")
// 模板跳转链接
.url(tmp.getUrl())
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "您已成功下单", "#888888"))
.addData(new WxMpTemplateData("time4", tmp.getGoodsName(), "#AAAAAA"))
.addData(new WxMpTemplateData("amount5", tmp.getPaymentAmount().setScale(2, RoundingMode.DOWN).toPlainString() + "零售币", "#AAAAAA"))
.addData(new WxMpTemplateData("thing3", tmp.getPaymentMethod() + "人", "#AAAAAA"))
.addData(new WxMpTemplateData("character_string2", tmp.getBuyTime() + "小时", "#AAAAAA"))
.addData(new WxMpTemplateData("remark", "详情请点击这里,祝生活愉快", "#0000FF"));
return templateMessage;
}
};
return messageHandler.send(tmp);
}
@Override
public boolean joinGroup(BeginGroupTmp tmp) {
AbstractTemplateMessageHandler<BeginGroupTmp> messageHandler = new AbstractTemplateMessageHandler<BeginGroupTmp>() {
@Override
WxMpTemplateMessage buildTemplate(BeginGroupTmp temp) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
// 接收者openid
.toUser(temp.getOpenid())
// 模板id 模板标题:开团成功通知
.templateId("begingroupSuccess")
// 模板跳转链接
.url(tmp.getUrl())
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "您已成功加入一个拼团", "#888888"))
.addData(new WxMpTemplateData("keyword1", tmp.getGoodsName(), "#AAAAAA"))
.addData(new WxMpTemplateData("keyword2", tmp.getGoodsPrice().setScale(2, RoundingMode.DOWN).toPlainString() + "零售币", "#AAAAAA"))
.addData(new WxMpTemplateData("keyword3", tmp.getGroupAmount() + "人", "#AAAAAA"))
.addData(new WxMpTemplateData("keyword4", tmp.getGroupTime() + "小时", "#AAAAAA"))
.addData(new WxMpTemplateData("remark", "详情请点击这里,祝生活愉快", "#0000FF"));
return templateMessage;
}
};
return messageHandler.send(tmp);
}
}
这样确实清爽一些了,每个方法里只实现了自己的特殊逻辑,其他的获取OpenId方法和判断是否发送方法都交给父类统一处理。
3、再次重构
我觉得定义20+个方法太麻烦了,能不能只用一个方法来接收不同的模板,将参数初始化放在模板中?
模板父类的改动
这里改动了一下TmpBase,增加两个抽象方法,让具体的子类自己实现
@Data
@EqualsAndHashCode(callSuper = false)
public abstract class TmpBase implements Serializable {
/**
* 标准时间参数格式化
*/
SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final long serialVersionUID = 1L;
//userId,openid 填写其中一个
/**
* 接收者userId
*/
private String userId;
/**
* 用户点击消息跳转链接
*/
private String url;
/**
* 接收者openid
*/
private String openid;
public String getUrl() {
if (ObjectUtil.isNull(url)) {
return "";
}
return url;
}
/**
* 模板参数
* @return
*/
public abstract Map<String, String> templateParameters();
/**
* 模板ID
* @return
*/
public abstract String templateId();
}
子类模板的改动
/**
* 购买成功消息推送模板
*
* 模板示例
* 订单支付成功通知
*
* 订单号 G202205230000010
* 商品名称 矿泉水
* 支付方式 微信支付
* 支付金额 200.00
* 下单时间 2022年11月11日 12:12
*
* 点击查看订单
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BuySuccessTmp extends TmpBase implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 购买时间
*/
private Date buyTime;
/**
* 商品名称
*/
private String goodsName;
/**
* 支付金额
*/
private BigDecimal paymentAmount;
/**
* 付款方式(零售币/积分/微信支付。。)
*/
private String paymentMethod;
/** 订单号 */
private String orderNo;
@Override
public Map<String, String> templateParameters() {
Map<String, String> parameter = new HashMap<>(8);
parameter.put("character_string2", this.orderNo);
parameter.put("thing3", this.orderNo);
parameter.put("thing6", this.paymentMethod);
parameter.put("amount5", this.paymentAmount.setScale(2, RoundingMode.DOWN).toPlainString());
parameter.put("time4", SIMPLE_DATE_FORMAT.format(this.buyTime));
return parameter;
}
@Override
public String templateId() {
return "buySuccess";
}
}
@Data
@EqualsAndHashCode(callSuper = false)
public class BeginGroupTmp extends TmpBase implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 商品名称
*/
private String goodsName;
/**
* 商品价格
*/
private BigDecimal goodsPrice;
/**
* 成团人数
*/
private int groupAmount;
/**
* 组团时间限制 以小时为单位
*/
private int groupTime;
@Override
public Map<String, String> templateParameters() {
Map<String, String> parameter = new HashMap<>(8);
parameter.put("first", "您已成功加入一个拼团");
parameter.put("keyword1", this.goodsName);
parameter.put("keyword2", this.goodsPrice.setScale(2, RoundingMode.DOWN).toPlainString() + "零售币");
parameter.put("keyword3", this.groupAmount + "人");
parameter.put("keyword4", this.groupTime + "小时");
parameter.put("remark","详情请点击这里,祝生活愉快");
return parameter;
}
@Override
public String templateId() {
return "begingroupSuccess";
}
}
具体的消息发送方法的改动
/**
* 微信模板消息发送方法
* @author YearOfTheRain
* @create 2024-06-17 14:36
*/
public interface WxTemplateMsgService {
/**
* 购买成功通知
*/
<T extends TmpBase> boolean send(T buySuccessTmp);
}
/**
* 实现方法
* @author YearOfTheRain
* @create 2024-06-17 14:38
*/
@Service
@Slf4j
public class WxTemplateMsgServiceImpl implements WxTemplateMsgService {
@Override
public <T extends TmpBase> boolean send(T tmp) {
AbstractTemplateMessageHandler<T> messageHandler = new AbstractTemplateMessageHandler<T>() {
@Override
WxMpTemplateMessage buildTemplate(T temp) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
// 接收者openid
.toUser(temp.getOpenid())
// 模板id 模板标题:支付成功通知
.templateId(temp.templateId())
// 模板跳转链接
.url(temp.getUrl())
.build();
// 通过在对象层面组装好模板参数,简化方法
Map<String, String> templateParameter = temp.templateParameters();
templateParameter.forEach((key,value)-> templateMessage.addData(new WxMpTemplateData(key, value)));
return templateMessage;
}
};
return messageHandler.send(tmp);
}
}
从对应的模板中获取对应的参数,进行组装处理
4、测试类
不需要改动的客户端
三个的测试类基本一致,客户端完全不需要改动(最后一次改了接口签名,其实也可以不改,但是不改语义不够好)
/**
* @author YearOfTheRain
* @create 2024-06-17 15:05
*/
@SpringBootTest
public class WxTemplateMsgServiceTest {
@Autowired
private WxTemplateMsgService wxTemplateMsgService;
@Test
void testBuySuccess() {
BuySuccessTmp buySuccessTmp = new BuySuccessTmp();
buySuccessTmp.setBuyTime(new Date());
buySuccessTmp.setGoodsName("111");
buySuccessTmp.setPaymentAmount(new BigDecimal("0.99"));
buySuccessTmp.setPaymentMethod("222");
buySuccessTmp.setOrderNo("111");
buySuccessTmp.setUserId("111");
buySuccessTmp.setUrl("111");
buySuccessTmp.setOpenid("1111");
wxTemplateMsgService.send(buySuccessTmp);
}
@Test
void testJoinGroup() {
BeginGroupTmp beginGroupTmp = new BeginGroupTmp();
beginGroupTmp.setGoodsPrice(new BigDecimal("30"));
beginGroupTmp.setGroupTime(20);
beginGroupTmp.setGoodsName("111");
beginGroupTmp.setGroupAmount(30);
beginGroupTmp.setUserId("111");
beginGroupTmp.setUrl("111");
beginGroupTmp.setOpenid("1111");
wxTemplateMsgService.send(beginGroupTmp);
}
}
5、改造完成
我的收获
改造后代码量急剧下降,20+个方法浓缩为一个方法。
而且更容易拓展,我现在如果要加一个模板只需要在加一个具体的模板实现,然后直接调用send() 方法即可。
项目源码地址
项目仓库所在目录
考虑到国内github访问缓慢,同步了一份到gitee 项目源码地址