支付系统 - 通道服务的框架设计演化

通道服务的框架设计演化

前言


大家都知道,和三方系统进行交互,往往会因为三方接口的设计对我们系统造成一定的侵入。这种侵入指的是,三方接口升级/三方接口设计不合理,导致的自身系统不兼容。遇到这种情况,系统会逐渐演变为打补丁的形态。随着补丁数的增多,原先的很多设计都被掩盖,代码中充斥着大量的 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);
    };
}

然后让通道继承这个类重写自己需要实现的方法即可:

扫描二维码关注公众号,回复: 11364895 查看本文章
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 接口的设计。此外,本文都是本人设计经验的一些总结,难免有不足之处,如果有不合理、不足之处、可以改进之处,还望指正,期盼探讨,谢谢大家。

猜你喜欢

转载自www.cnblogs.com/pleuvoir/p/13192821.html