• 支付系统


    通道服务的框架设计演化

    前言


    大家都知道,和三方系统进行交互,往往会因为三方接口的设计对我们系统造成一定的侵入。这种侵入指的是,三方接口升级/三方接口设计不合理,导致的自身系统不兼容。遇到这种情况,系统会逐渐演变为打补丁的形态。随着补丁数的增多,原先的很多设计都被掩盖,代码中充斥着大量的 If else 到最后维护起来都困难,一个很简单的逻辑隐藏在各种判断之中,因为这些细节补丁一叶蔽目。

    那么,通道服务具备高扩展性设计就是我们需要考虑的重点。为了说明这个系统演化的过程,我准备从初版逐步过渡,最终给出一个我认为比较合理的设计。当然这个设计也是不完美的,希望读者能在演化的过程中得出自己的思考。本文会以将故事的形式进行,以下是正文(纯属虚构)。

    v1 简单工厂 + 策略模式


    五月二十那一日天气晴朗,小希一到公司发现前台姐姐对他温柔了不少,想到平时小姐姐对自己都是爱答不理。单身多年的小希心中乐开了花,有那么一刻孩子姓甚名谁都想好了。回过神来,用刚吃完油条的手摸了摸“我变秃了,但没变强”的脑袋,心中有那么一丝寥落暗生。可不巧小希平日是个很传统的人,心中不禁犯了嘀咕:“今日想必一定有大事发生”。念罢,迈起步子走上二楼办公大堂。

    “好啊,早起的鸟儿有虫吃!” 老王笑呵呵的说道,“最近咱公司准备做一套支付系统,你来负责对接三方通道的部分,行不行?”。“男人怎么能说不行?” 小希赶紧应承下来,转念一想 “我没搞过啊,装 X 一时爽,算了硬搞吧,原来这就是今天的大事啊,上天安排的果然最大!”。

    快乐的时光总是这么的短暂,平日里沉默不语酷爱划水看小说的小希渐渐变得暴躁起来。“CIAO,什么鬼,啥玩意,这东西写的和个 SHIT 一样”时不时的从他的工位传来。凑近一看,原来小希正遨游在微信和支付的官网文档里不能自拔。“厉害啊小希,都整上支付了”大哥淡淡的说道。“哎,不太行,微信支付写的一般,要我重新设计绝对比他好一百倍” 吹完牛,小希又埋头钻进官网去了。

    经过三天三夜的连续苦战,小希似乎已经知道了通道系统设计的诀窍。“嗨呀,不就是一个简单工厂加一个接口服务吗?有什么难的!太看不起我天下第一绝顶希老板了”。于是,一个设计草稿就画出来了。


    此时路过的大哥不小心瞟见了,看了半天后说道:“你这个设计很妙啊,将所有的通道接口都封装在一个类里面,这样不同的通道只需要实现这个接口就行了。通过传入通道编号再通过工厂方法来获得这个通道服务,然后调用指定的方法。问一下,这是工厂方法加策略模式吗?高,实在是高!” 。“必须的,我可是精通设计模式的辣个男人,吊不吊?” 小希淡淡的对大哥说道,但其实内心兴奋早已压制不住,终于逮住机会让我炫一波技了。“不过我还有个疑问,最前面的 NetPayServiceImplJsPayServiceImpl 是前面的入口这我可以理解,但是 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 服务插件 + 服务收口 + 泛型设计


    向着前路进发。知道了之前把所有支付方式糅合在一个借口中设计的弊端,那么改进方法就很明确了,一个字就是拆。怎么拆呢?小希心中又犯了嘀咕。

    1. 是需要按照业务划分为组?比如微信相关的弄到一起,比如微信组里包含 H5、WAP、扫码、查询等。如果是这样,一个通道可能会实现多个组。理论上是可以的,但是这样有分组的麻烦,并且改动 H5 可能会影响扫码因为代码在一个类里。
    1. 还是直接每个方法一个类,我想办法直接让程序能调用到这个方法。设计一个顶层接口,通过泛型入参泛型返回,利用工厂方法选择具体的实现


    经过考虑,小希选择了第二种。下面来看下他的代码实现:

    首先是顶层接口的设计:

    /**
     * 抽象通道服务接口
     */
    public interface IChannelService<T extends AbstractReqModel, R extends AbstractRspModel> {
    
        R invoke(T request) throws ChannelServiceException;
    }
    

    AbstractReqModelAbstractRspModel 没什么好说的,就是定义了入参和出参的父类,里面有一些公共的变量。有了这层顶层接口,问题的关键就转化成了如何获取不同的 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 接口的设计。此外,本文都是本人设计经验的一些总结,难免有不足之处,如果有不合理、不足之处、可以改进之处,还望指正,期盼探讨,谢谢大家。

  • 相关阅读:
    3.java开发环境配置
    2.java主要特性
    1.java中main函数理解
    测试项目团队角色岗位职责
    单身程序员
    软件测评师考试
    vue父子组件通信
    python偏函数使用
    Numpy+Pandas读取数据
    chrome无界面模式headless配置
  • 原文地址:https://www.cnblogs.com/pleuvoir/p/13192821.html
Copyright © 2020-2023  润新知