zoukankan      html  css  js  c++  java
  • 支付系统

    通道服务的框架设计演化

    前言


    大家都知道,和三方系统进行交互,往往会因为三方接口的设计对我们系统造成一定的侵入。这种侵入指的是,三方接口升级/三方接口设计不合理,导致的自身系统不兼容。遇到这种情况,系统会逐渐演变为打补丁的形态。随着补丁数的增多,原先的很多设计都被掩盖,代码中充斥着大量的 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 接口的设计。此外,本文都是本人设计经验的一些总结,难免有不足之处,如果有不合理、不足之处、可以改进之处,还望指正,期盼探讨,谢谢大家。

  • 相关阅读:
    HDU 2196 Computer
    HDU 1520 Anniversary party
    POJ 1217 FOUR QUARTERS
    POJ 2184 Cow Exhibition
    HDU 2639 Bone Collector II
    POJ 3181 Dollar Dayz
    POJ 1787 Charlie's Change
    POJ 2063 Investment
    HDU 1114 Piggy-Bank
    Lca hdu 2874 Connections between cities
  • 原文地址:https://www.cnblogs.com/pleuvoir/p/13192821.html
Copyright © 2011-2022 走看看