通道服务的框架设计演化
前言
大家都知道,和三方系统进行交互,往往会因为三方接口的设计对我们系统造成一定的侵入。这种侵入指的是,三方接口升级/三方接口设计不合理,导致的自身系统不兼容。遇到这种情况,系统会逐渐演变为打补丁的形态。随着补丁数的增多,原先的很多设计都被掩盖,代码中充斥着大量的 If else 到最后维护起来都困难,一个很简单的逻辑隐藏在各种判断之中,因为这些细节补丁一叶蔽目。
那么,通道服务具备高扩展性设计就是我们需要考虑的重点。为了说明这个系统演化的过程,我准备从初版逐步过渡,最终给出一个我认为比较合理的设计。当然这个设计也是不完美的,希望读者能在演化的过程中得出自己的思考。本文会以将故事的形式进行,以下是正文(纯属虚构)。
v1 简单工厂 + 策略模式
五月二十那一日天气晴朗,小希一到公司发现前台姐姐对他温柔了不少,想到平时小姐姐对自己都是爱答不理。单身多年的小希心中乐开了花,有那么一刻孩子姓甚名谁都想好了。回过神来,用刚吃完油条的手摸了摸“我变秃了,但没变强”的脑袋,心中有那么一丝寥落暗生。可不巧小希平日是个很传统的人,心中不禁犯了嘀咕:“今日想必一定有大事发生”。念罢,迈起步子走上二楼办公大堂。
“好啊,早起的鸟儿有虫吃!” 老王笑呵呵的说道,“最近咱公司准备做一套支付系统,你来负责对接三方通道的部分,行不行?”。“男人怎么能说不行?” 小希赶紧应承下来,转念一想 “我没搞过啊,装 X 一时爽,算了硬搞吧,原来这就是今天的大事啊,上天安排的果然最大!”。
快乐的时光总是这么的短暂,平日里沉默不语酷爱划水看小说的小希渐渐变得暴躁起来。“CIAO,什么鬼,啥玩意,这东西写的和个 SHIT 一样”时不时的从他的工位传来。凑近一看,原来小希正遨游在微信和支付的官网文档里不能自拔。“厉害啊小希,都整上支付了”大哥淡淡的说道。“哎,不太行,微信支付写的一般,要我重新设计绝对比他好一百倍” 吹完牛,小希又埋头钻进官网去了。
经过三天三夜的连续苦战,小希似乎已经知道了通道系统设计的诀窍。“嗨呀,不就是一个简单工厂加一个接口服务吗?有什么难的!太看不起我天下第一绝顶希老板了”。于是,一个设计草稿就画出来了。
此时路过的大哥不小心瞟见了,看了半天后说道:“你这个设计很妙啊,将所有的通道接口都封装在一个类里面,这样不同的通道只需要实现这个接口就行了。通过传入通道编号再通过工厂方法来获得这个通道服务,然后调用指定的方法。问一下,这是工厂方法加策略模式吗?高,实在是高!” 。“必须的,我可是精通设计模式的辣个男人,吊不吊?” 小希淡淡的对大哥说道,但其实内心兴奋早已压制不住,终于逮住机会让我炫一波技了。“不过我还有个疑问,最前面的 NetPayServiceImpl
和 JsPayServiceImpl
是前面的入口这我可以理解,但是 ChannerlCenterServiceImpl
是做什么的?” 大哥谦虚的问道。“这个嘛,所有通道的统一入口,你可以在里面做很多事情,虽然我现在只是在里面打了下日志,但这是为以后扩展设计,懂我意思吧?”。听罢,大哥点了点头端着自己的枸杞茶默默的回到了自己的工位,果然我还是太菜啊,心里默念道。
v2 适配器模式
岁月如歌,时光如梭。没想到业务发展的越来越好,这套支付系统已经成为了公司的核心项目,老王都有了带着小希跳槽单独成立一家支付公司的念头。伴随着越来越多的三方通道, IChannelService
中的方法也不可避免的膨胀了,小希也逐渐意识到了这个问题。我们来看看设计之初预想的接口模样:
public interface IChannelService {
//网银支付
ResultNetDTO netPay(NetPayDTO netpayDto);
//获取二维码
ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto);
//订单查询
ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto);
}
没错,按照之前预想的我们不会有太多的支付方式,可是千算万算没想到,现在已经对接了十几家三方通道,每家的支付方式都有所不同。尤其是快捷支付,PM 都没办法区分不同通道快捷支付的差异了,索性就叫 1,2,3 了。小希虽然百般不愿意,也只能写出了这样的代码:
//别骂我,产品就这么起的名字
ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto);
ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto);
ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto);
这还不是最要命的,一个名字起的烂无所谓。关键是之前的微信和支付宝,压根就没有这些个快捷支付,还要求我实现这些方法,这太不合理了。我们来看看微信现在的模样:
public class WechatChannelServiceImpl implements IChannelService{
@Override
public ResultNetDTO netPay(NetPayDTO netpayDto){
//省略实现....
};
@Override
public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){
//省略实现....
};
@Override
public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){
//省略实现....
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
}
这个咋整,聪明的小希一下子就想到了适配器模式,或者使用 JDK8 的 default
语法。
public class ChannelServiceAdapter implements IChannelService{
@Override
public ResultNetDTO netPay(NetPayDTO netpayDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay1(QuickPay1DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay2(QuickPay2DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
@Override
public ResultQuickPayB2cChannelMsgDTO quickPay3(QuickPay3DTO payDto){
throw new ChannelServiceException(ResultCode.NOT_PROVIDE_FUNCTION);
};
}
然后让通道继承这个类重写自己需要实现的方法即可:
public class WechatChannelServiceImpl extends ChannelServiceAdapter{
@Override
public ResultNetDTO netPay(NetPayDTO netpayDto){
//省略实现....
};
@Override
public ResultQrCodeDTO payCode(QrCodeDTO qrCodeDto){
//省略实现....
};
@Override
public ResultQueryChannelDTO orderQuery(ChannelOrderQueryDTO orderQueryDto){
//省略实现....
};
}
这样是不是也可以呢?小希觉得是暂时掩盖了子类强制让实现父类方法的恶心之处,并没有实际解决问题。
v3 服务插件 + 服务收口 + 泛型设计
向着前路进发。知道了之前把所有支付方式糅合在一个借口中设计的弊端,那么改进方法就很明确了,一个字就是拆。怎么拆呢?小希心中又犯了嘀咕。
是需要按照业务划分为组?比如微信相关的弄到一起,比如微信组里包含 H5、WAP、扫码、查询等。如果是这样,一个通道可能会实现多个组。理论上是可以的,但是这样有分组的麻烦,并且改动 H5 可能会影响扫码因为代码在一个类里。
还是直接每个方法一个类,我想办法直接让程序能调用到这个方法。设计一个顶层接口,通过泛型入参泛型返回,利用工厂方法选择具体的实现?
经过考虑,小希选择了第二种。下面来看下他的代码实现:
首先是顶层接口的设计:
/**
* 抽象通道服务接口
*/
public interface IChannelService<T extends AbstractReqModel, R extends AbstractRspModel> {
R invoke(T request) throws ChannelServiceException;
}
AbstractReqModel
和 AbstractRspModel
没什么好说的,就是定义了入参和出参的父类,里面有一些公共的变量。有了这层顶层接口,问题的关键就转化成了如何获取不同的 IChannelService
实现,注意:这里的实现和之前有所不同,我们来看几个实现类。
//微信扫码
public class WechatScanCode implements IChannelService<PaymentDTO, PaymentResultDTO> {
@Override
public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {
// 省略实现
return null;
}
}
//微信手机网站支付
public class WechatMobileH5 implements IChannelService<PaymentDTO, PaymentResultDTO> {
@Override
public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {
// 省略实现
return null;
}
}
//支付宝APP支付
public class AlipayApp implements IChannelService<PaymentDTO, PaymentResultDTO> {
@Override
public PaymentResultDTO invoke(PaymentDTO request) throws ChannelServiceException {
// 省略实现
return null;
}
}
//支付宝支付结果查询
public class AlipayPayQuery implements IChannelService<PayQueryDTO, PayQueryResultDTO> {
@Override
public PayQueryResultDTO invoke(PayQueryDTO request) throws ChannelServiceException {
// 省略实现
return null;
}
}
OK,相信读者对小希的设计现在有了清晰的了解。即:每一个和三方需要交互的都会新建一个类去实现,每个类的入参和出参都不同。有的小朋友可能就要问了:“小希,这么搞会不会类爆炸,写起来也好麻烦啊?”。没错,确实类会变的多一些,但你想一下,这样每个接口的互不影响出 BUG 的几率也大大的下降,并且找起来也容易。综合取舍一下,我觉得还是值得!
有了这个顶层接口,按照之前的设计,我们同样可以通过工厂方法来确定具体的服务实现类。唯一的不同是,之前的工厂方法只有一个入参:通道编号。这次我们需要增加一个新伙伴:
public enum ServiceIdEnum {
//扫码支付
SCAN_CODE("scan_code"),
//APP支付
APP("app"),
//付款码支付
BRUSH_CARD("brush_card"),
//公众号支付
GZ("gz"),
//小程序支付
MINI_PROGRAM("mini_program"),
//手机网站支付
MOBILE_H5("mobile_h5"),
//电脑网站支付
PC_WEB("pc_web"),
//支付查询
PAY_QUERY("pay_query"),
//退款查询
REFUND_QUERY("refund_query"),
//支付通知
PAY_NOTIFY("pay_notify"),
//省略后续
这个枚举主要作用就是用来确认调用哪个类的(不要去想着他是什么分类,完全是程序实现需要)。
工厂方法通过通道和服务 ID 就能找到对应的类:
public interface IChannelServiceFactory {
/**
* 通过通道和服务类别获取通道服务,未获取到时返回null
*/
IChannelService getChannelService(ChannelEnum channel, ServiceIdEnum serviceId);
}
这样,我们可以通过这样的方式来调用:
@Component
@Slf4j
public class ServiceDispatcher {
@Autowired
private IChannelServiceFactory channelServiceFactory;
private IChannelLifeCycleListener lifeCycleListener = new IChannelLifeCycleListener.Adapter();
public <R extends AbstractRspModel> R doDispatch(AbstractReqModel reqModel) throws ChannelServiceException {
final ChannelEnum channel = reqModel.getChannel();
final ServiceIdEnum serviceId = reqModel.getServiceId();
IChannelService channelService = channelServiceFactory.getChannelService(channel, serviceId);
if (channelService == null) {
log.error("获取通道服务失败 :) channel={},serviceId={}", JSON.toJSONString(channel), JSON.toJSONString(serviceId));
throw new ChannelServiceException(ReturnCodeEnum.ERROR, "获取通道服务失败");
}
log.info("获取通道服务成功。channelService={},serviceId={}", channelService.getClass().getSimpleName(),
JSON.toJSONString(serviceId));
lifeCycleListener.beforeRequest(reqModel);
StopWatch watch = StopWatch.createStarted();
R rspModel = null;
try {
rspModel = (R) channelService.invoke(reqModel);
} catch (ChannelServiceException e) {
lifeCycleListener.exceptionCaught(e);
throw e;
}
watch.stop();
lifeCycleListener.afterRequest(reqModel, rspModel);
log.info("调用通道服务成功,耗时{}(毫秒)。reqModel={},rspModel={}",
watch.getTime(TimeUnit.MILLISECONDS),
JSON.toJSONString(reqModel),
JSON.toJSONString(rspModel));
return rspModel;
}
}
可以看到,这些参数全部由上游调用方传递,另外,这里加了个监听器,小希的代码还没写完,是预留给以后统计服务 QOS 等用的。
很多小朋友可能会问了,具体的通道服务实现是怎么做的?熟悉 Spring
的朋友可能都知道, Spring
容器本身就有扫描包的功能,再加上动态注册 Bean
很容易就能实现这个功能。只需要做一个自定义注解,再加上自动扫描注册时设置一个别名,以后通过该别名即可拿到这个 Bean
。但是,这种方式的缺点是不好维护,因为每次都需要自己在脑子去想这个类在哪,对新来的小伙伴不友好。
所以小希返璞归真想了一个最常规的方法,那就是手动在 XML
中配置,同时按通道进行区分,看图:
channel-alipay.xml
中是这样的:
这样新来的小伙伴也能很快的找到对应的类了,可维护性高达 9 个 9,想到这里小希为自己的机智买了杯肥宅快乐水。
那这里一个关键点是保存映射关系的类,要知道我们需要通过通道编号,服务 ID 拿到具体的实现类,常规的实现是 Map<ChannelEnum,Map<ServiceIdEnum,IChannelService>>
。有没有一种优雅的数据结构可以直接把这种关系囊括进去呢?答案是 Yes。
小希又能炫技了,是时候请出我们的大哥 Guava
了,直接看代码:
//通道、服务类别,实现类
private Table<ChannelEnum, ServiceIdEnum, Class<?>> services = Tables.newCustomTable(new ConcurrentHashMap<>(), ConcurrentHashMap::new);
这是 Guava
中提供的类似 Excel
的数据结构,叫做三元组。其中 1、2、3 位分别表示行、列、值。这样说我想大家都应该明白了。有了这个实现类,把它做到 Spring
中还不是分分钟的事情。
public void afterPropertiesSet() {
BeanDefinitionRegistry beanRegistry = (BeanDefinitionRegistry) applicationContext;
channelServicePlugin.getChannels().forEach((ChannelEnum channel) -> { //遍历服务插件,服务插件负责解析XML,提供 通道、服务类别,实现类的对应关系
channelServicePlugin.getServiceMap(channel).forEach((ServiceIdEnum serviceId, Class<?> service) -> {
String serviceName = channel.name() + "_" + service.getSimpleName();
BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.genericBeanDefinition(service);
beanRegistry.registerBeanDefinition(serviceName, beanBuilder.getBeanDefinition()); //将对应的实现类注册到容器中
channelTransServiceTable.put(channel, serviceId, (IChannelService) applicationContext.getBean(serviceName)); //将刚注册Bean实例加入三元组中
});
});
}
如此,所有的设计就完成了。小希开心的看着这些设计,嘴角露出了满意的微笑。
后语
支付通道的服务设计还需要考虑每个通道的请求参数以及文件获取,本文中没有讨论,这些参数获取也应该是此服务独立完成,不应该由上层调用方传递。譬如一些三方接口的版本问题,一句话两句话也说不清楚,所以文中也没有考虑如支付宝 V1,支付宝 V2 接口的设计。此外,本文都是本人设计经验的一些总结,难免有不足之处,如果有不合理、不足之处、可以改进之处,还望指正,期盼探讨,谢谢大家。