buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

程序设计---对外接口可不仅仅是“给大佬递餐”,前置工作还是要做滴~

【先举个栗子🌰】方法传参方式:直接下传业务参数 vs 下传解析后的业务参数

先看下面代码中的两个方法。executedoPaymentAuthResultQuery,一个方法接收到参数后,直接将参数原样传递给另一个方法。

@Override
public void execute(String jobParameter) {
    log.info("亿联绑卡打款认证结果查询-定时任务:{}", jobParameter);
    try {
        doPaymentAuthResultQuery(jobParameter);
    } catch (Exception e) {
        log.error("亿联绑卡打款认证结果查询-定时任务异常:", e);
    }
}

private void doPaymentAuthResultQuery(String jobParameter) {
    String startTime, endTime;
    if (StringUtil.isNotEmpty(jobParameter)) {
        JSONObject jsonObject = JSON.parseObject(jobParameter);
        startTime = jsonObject.getString("startTime");
        endTime = jsonObject.getString("endTime");
    } else {
        Date date = new Date();
        startTime = DateUtil.offsetDay(date , -10).getDate();
        endTime = DateUtil.formatDate(date);
    }

    //查询处理中的绑卡流水
    LambdaQueryWrapper<PayMerchantBankCardFlow> bankCardFlowQuery = new LambdaQueryWrapper<PayMerchantBankCardFlow>()
            .eq(PayMerchantBankCardFlow::getStatus, StatusEnum.DEALING.getCode())
            .between(PayMerchantBankCardFlow::getCreateTime, startTime, endTime);

    List<PayMerchantBankCardFlow> listBankCardFlow = payMerchantBankCardFlowManager.list(bankCardFlowQuery);
    
    ...
}
View Code

然后,我们把这段代码稍作改动,主要是变更了第二个被调方法 doPaymentAuthResultQuery 的参数。大家来比较一下,改动前后,哪个更优一些。

@Override
public void execute(String jobParameter) {
    log.info("亿联绑卡打款认证结果查询-定时任务:{}", jobParameter);
    try {
        String startTime, endTime;
        if (StringUtil.isNotEmpty(jobParameter)) {
            JSONObject jsonObject = JSON.parseObject(jobParameter);
            startTime = jsonObject.getString("startTime");
            endTime = jsonObject.getString("endTime");
        } else {
            Date date = new Date();
            startTime = DateUtil.offsetDay(date , -10).getDate();
            endTime = DateUtil.formatDate(date);
        }
        doPaymentAuthResultQuery(startTime, endTime);
    } catch (Exception e) {
        log.error("亿联绑卡打款认证结果查询-定时任务异常:", e);
    }
}

private void doPaymentAuthResultQuery(String startTime, String endTime) {

    //查询处理中的绑卡流水
    LambdaQueryWrapper<PayMerchantBankCardFlow> bankCardFlowQuery = new LambdaQueryWrapper<PayMerchantBankCardFlow>()
            .eq(PayMerchantBankCardFlow::getStatus, StatusEnum.DEALING.getCode())
            .between(PayMerchantBankCardFlow::getCreateTime, startTime, endTime);

    List<PayMerchantBankCardFlow> listBankCardFlow = payMerchantBankCardFlowManager.list(bankCardFlowQuery);
    
    ...
}
View Code

 

一句话:良好的接口参数处理是高质量代码的重要特征。通过将原始参数尽早转换为业务语义明确的参数,我们可以提高代码的可读性、可维护性和可测试性。

 

【“调用方可能是不可信的”————Fail-Fast程序设计原则】

在接收到请求参数后,我们的程序应该先进行参数的合法性校验,包括非空判断、数据格式校验以及数据有效性校验,然后再执行后续的业务逻辑。 尤其对于分布式微服务系统,更要先进行参数的合法性校验,再调用后续的RPC接口。关于这个基本的程序设计原则,我相信大家对此没有什么异议。

当然,除了参数合法性校验,服务层或RPC层还应包括参数的业务校验,如用户是否存在、用户状态是否有效、订单是否存在。

这一核心设计原则又称为​​“Fail-Fast”(快速失败)​,这是所有健壮性软件设计的基石。一个健壮的软件系统,参数的防御性校验是不可或缺的(参数合法性校验+参数的业务校验)。

【单就RPC调用,也有必要说道说道——“先校验,再RPC”】

我们来看一个案例。

电商网站中,用户在订单详情页点击“确认支付”,发起余额支付。

 

这里,我们做如下3项设定。

1)网站后台SpringMVC程序暴露的“订单支付”接口名为 order/pay。

2)后台程序对于“支付”的处理逻辑,我们简化成下面的业务流程。

3)后台程序是分布式的微服务结构,包括提供REST接口的SpringMVC服务和后面提供RPC接口的订单服务、账务服务。

 

那么, 比较下面两种实现方式。左边第一种是在order/pay这个REST接口里先查单校验订单状态,通过后才调用订单服务的“支付订单”RPC接口。右边第二种是直接转发请求给订单服务的“支付订单”RPC接口。你更倾向于哪一种实现方式呢?

 

相比来说,我认为第一种更靠谱一些。

Why?

看上去,虽然两种实现方式都能达到目的,第一种方式还多了一个前置的校验。为什么我建议采用第一种方式呢?

这是典型的程序业务处理的方式。——接收到请求入参后,先进行前置校验,如果校验失败直接中止返回,否则才走后续的业务处理流程。

 

有同学就说了,按第二种实现方式,直接调用订单服务的“支付订单”RPC接口,“支付订单”RPC接口的实现里不是也有订单状态的前置校验吗?

这是有区别的。按第一种实现方式的话,支付订单RPC接口除了流程图里的实现方式外,也可以省掉查询订单这一步,直接通过包含状态机幂等的update操作来变更订单状态(sql诸如UPDATE order SET pay_status='PAYING' WHERE order_no = '001' AND pay_status = 'INIT'),根据update是否成功来决定后面的扣减用户余额的逻辑。如果采用第二种实现方式,那就最好先老老实实的通过查询订单来判断状态,毕竟数据库的update开销比select开销要高。

从技术的角度来分析,两种实现方式也是有区别的。“支付订单”作为一个业务处理的RPC接口,我们要做的控制会比较多,例如事务控制、幂等、异常处理、耗时、锁、监控,等等。因此,从这个角度来看,客户端在确认需要支付订单的时候再调用“支付订单”,是不是更合理呢?

 

类似的案例,也包括,我们的MQ消费者,在从队列里拿到消息后,先进行必要的判断和校验,然后再调用业务方法。而不是一上来就直接把参数丢给业务方法。

 

本文设计文稿物料:https://www.processon.com/view/link/611e38c2e0b34d3511f7c479

 


 

 

花絮:

人力外包需求开发完成后,我们那天上午在评审代码时,针对“发起结算”这个controller方法是否做前置校验的事情上出现分歧。Yamei和Haipeng等人认为没必要做前置校验,反正后面调用的dubbo接口的实现里也会做前置校验。尽管我做了一些解释,终未能达成共识。 下午继续评审代码时,看到一个mq的listener方法里,解析到业务单号后,直接将业务单号作为入参调用业务处理方法。Yamei建议这个listener方法先验证业务单据,验证通过后再调用业务处理方法。  我向她确认了她是这个观点后,类比了上午的争论,我俩相视一笑。举一反三,多么重要。

 

分享:

经验这个东西,往往并不能告诉我们什么一定对,但是可以告诉我们什么一定不对。----《The Pragmatic Programmer,程序员修炼之道:从小工到专家》

posted on 2023-10-15 18:27  buguge  阅读(148)  评论(0)    收藏  举报