日常工作中的策略模式轻实践
0.引言
在座的各位应该都知道,在日常的面试中,设计模式都是面试官屡试不爽的面试主题,借这个话题,面试官可以从面试者的回答中了解他对设计模式的熟悉程度,是否有自己的理解,更可以挖掘平时是否付诸实践,从而初步判断面试者对代码解耦的理解程度。今天,我们就来谈谈其中之一也是最常见的设计模式-策略模式,在我们的日常工作中如何运用上,达到轻松解耦(zhuangbei)的目的。
1.场景简述
基本在每个设计支付的项目中,我们都会涉及到这个场景:
用户用手机下单之后,支付请求发送至服务端接口,api接口根据请求参数判断为属于支付宝支付还是微信支付,又或者是京东支付或者银联支付,经过校验\计算订单金额等通用流程后,来调用第三方支付接口进行支付和支付回调。这里包括我可能都选择第2点的做法,我们来看:
2.一般做法
在controller层的接口进入服务端,再经过分布式锁之后,调用service层来执行相应的业务逻辑,而后通过if else的方式分别进入不同的方法来调用第三方预支付接口,大概通过以下伪代码可以看出:
// 伪代码 1.支付宝 2.微信 3....
public void process(PayRequest payRequest){
// 1.校验 根据不同支付通道来进行不同的校验
// 需要if else
validate(payRequest);
// 2.计算金额 这里可能因为支付方式的不同
// 也需要if else计算金额
calculate(payRequest);
// 3.创建订单 这里因为业务不同,例如订单记录支付通道
// 所以也需要if else
createOrder(payRequest);
...
// n.调用第三方支付接口
// 需要if else
if(channel == 1){
aliRequest(payRequest);
}else if(channel == 2){
wxPayRequest(payRequest);
}
...
}
private void aliRequest(payRequest){
// 调用支付宝第三方支付预创建接口
}
private void wxPayRequest(payRequest){
// 调用微信第三方支付预创建接口
}
...
相信大多数人都是都是这样写的,包括笔者自己,因为确实没有一些问题,毕竟只是简单的判断,if else就完事了,还要啥自行车?
但是去年,因为需求我需要做支付宝小程序支付,需要在支付中信拓展支付宝小程序支付时,看了下同事支付端的代码结构,发现自己在集成时非常方便,只需要实现支付接口拓展一个甚至不需要动同事的代码再加上一些业务代码即可,他省事我也省事。顿时觉得自己还是太年轻了。
3.别样思考
3.1 缘起
在做完这个需求后的最近,支付同事离职了,自己暂时接管支付这部分的内容,所以也对这部分代码进行了复盘,受益良多。
大家应该有听过开闭原则:对修改关闭,对扩展开放。这个原则是说,在设计一个软件模块时,应该使这个模块可以在不被修改的前提下被扩展。换句话说,就是应该可以在不修改原有源代码的情况下,改变这个模块的行为。我感觉,这个结构还是听符合开闭原则的,让编码者码着舒服让参与者无压力加入。
3.2 代码结构图
3.3 简单说明
strategy模块可以从以下图中看出端倪:
首先定义了一个PayStrategy接口类,声明了基本的方法
public interface PayStrategy {
/*
* 校验
* @param payRequest
*/
void validate(PayRequest payRequest);
/*
* 处理支付请求
* @param payRequest
* @return
*/
void process(PayRequest payRequest);
/*
* 请求第三方支付接口
* @param payRequest
*/
void request(PayRequest payRequest);
}
这里其中,request就是前面所谈能够替代if else的核心🔑。
接着,当然是得创建类来实现这个接口了,不过再这之前需要对这个接口做一层抽象,作用的话我们稍后再谈。
@Slf4j
public abstract class BasePayStrategy implements PayStrategy {
/*
* 缓存<支付通道,支付实例>
*/
protected static Map<Integer, PayStrategy> payMap = new ConcurrentHashMap<>(16);
/*
* 根据支付通道获取支付策略实例
* @param channel
* @return
*/
public static PayStrategy getPayStrategy(Integer channel){
return payMap.get(channel);
}
@Override
public void process(PayRequest payRequest) {
// 这里可以写通用流程
log.info("【此处BasePayStrategy】执行调用第三方支付接口前的通用流程");
// 1.校验
validateParams(payRequest);
// 2.计算支付金额
BigDecimal totalFee = calculateFee(payRequest);
// 3.预创建订单等...
preCreateOrder(payRequest,totalFee);
// n.调用第三方支付接口
request(payRequest);
}
private void validateParams(PayRequest payRequest){
log.info("这里校验通用的参数,例如是否为有效用户\商品是否有效已下架等");
// 不同通道的特定参数校验
validate(payRequest);
}
private BigDecimal calculateFee(PayRequest payRequest){
// 计算金额 如果也需要不同渠道计算金额也可和校验参数一样设计
return BigDecimal.ZERO;
}
private void preCreateOrder(PayRequest payRequest, BigDecimal totalFee){
// 预创建订单 如果也需要不同渠道计算金额也可和校验参数一样设计
}
}
可以从代码中看到,抽象类中做了一层缓存,而目的就是存储每个通道对应的策略实例类。在请求发起时,工厂类可以根据请求中的通道参数获取对应的策略实例类进行处理。(不小心简单工厂模式就上手了,代码看下方)
@Component
public class PayFactory {
@Autowired
private ApplicationContext context;
/**
* 获取对应支付类型的策略实例
* @param channel
* @return
*/
public PayStrategy getPayStrategy(Integer channel){
PayStrategy payStrategy = BasePayStrategy.getPayStrategy(channel);
if(payStrategy == null){
throw new ServiceException(FAIL_CODE,"无效支付通道");
}
return context.getBean(payStrategy.getClass());
}
}
使用工厂:
@Autowired
private PayFactory payFactory;
/**
* 这里直接省略了分布式锁\service类\聚合类,直接写在api方法上
* @param payRequest
* @return
*/
@PostMapping("\pay1")
public void pay1(@RequestBody PayRequest payRequest){
// 1.根据支付通道获取对应支付策略类
Integer channel = payRequest.getChannel();
PayStrategy payStrategy = payFactory.getPayStrategy(channel);
// 2.封装支付请求处理器(策略模式)
PayHandler payHandler = new PayHandler(payStrategy);
// 3.处理请求
payHandler.processRequest(payRequest);
}
继续这个抽象类,可以看出他只实现了PayStrategy的process方法,这个方法在这里起到中间层承上启下的作用,可以在这其中进行相关的校验\计算\订单创建等操作,再执行调用第三方接口-request方法,看下面两个具体的策略实现类
AliPayStrategy:
@Component
@Slf4j
public class AliPayStrategy extends BasePayStrategy {
@PostConstruct
public void initStrategy(){
payMap.put(AliPay.getPayChannel(),this);
}
@Override
public void validate(PayRequest payRequest) {
log.info("校验支付宝通道的特殊参数");
}
@Override
public void request(PayRequest payRequest) {
log.info("\n██【调用阿里支付接口】\n██接口参数:{}",payRequest);
// 调用阿里支付接口流程
}
}
WxPayStrategy:
@Component
@Slf4j
public class WxPayStrategy extends BasePayStrategy {
@PostConstruct
public void initStrategy(){
payMap.put(WxPay.getPayChannel(),this);
}
@Override
public void validate(PayRequest payRequest) {
log.info("校验微信通道的特殊参数");
}
@Override
public void request(PayRequest payRequest) {
log.info("\n██【调用微信支付接口】\n██接口参数:{}",payRequest);
// 调用微信支付接口流程
}
}
这两个就是具体的通道实现,在日志备注中可以看出相应的作用了吧!是不是觉得很简单!
3.4 测试与demo
当然,代码只有执行了才能印证一切
源码demo
启动项目,调用 http:\localhost:8081\pay 接口
怎么样,是不是觉得代码清新了许多!