02.卫语句代码规范

9 阅读7分钟

卫语句代码规范

1.介绍

在传统的代码编写中,存在一种很恶心代码结构:

85fda0e71177c10a0a768785c87a82ea346824570.jpg

使用卫语句可以优化这种代码结构

卫语句的核心思想就是:如果不符合条件,立刻滚蛋(return/throw),不要让非法流进入核心逻辑

它将正向判断(如果是A,则做B)转化为反向拦截(如果不是A,则停止),从而消除else分支,拉平代码结构

2.黄金三层级

介绍

一个健壮的方法体,其内部结构应该像一个漏斗,层层筛选,最后只留下合法的核心操作

防御层(NPE防御与基础校验)

介绍

主要做空指针防御和基础校验

目的

保护程序不崩溃

为什么排第一

因为后续的逻辑往往依赖于这些对象。如果你先去检查业务状态,而对象本身是NULL,程序直接空指针,业务检查就失去了意义

业务层(前置条件与权限校验)

介绍

检查用户权限,账户余额,订单状态,库存数量,只有满足业务条件才让继续执行

目的

保护业务规则不被破坏

为什么排第二

此时对象以确保非空,是安全的。但如果业务条件不满足(例如余额不足),继续执行核心逻辑(扣款)就是严重的bug

核心层

介绍

执行真正的核心业务逻辑,比如:状态更新,数据库落库,发送消息,计算逻辑

位置

方法的最后,通常没有任何缩进

3.实例对比:转账场景

介绍

假设我们需要写一个transfer方法:用户A向用户B转账

反例

    /**
     * 用户A向用户B转账(嵌套 if else 实现)
     * @param userA 转账人
     * @param userB 收款人
     * @param amount 转账金额
     * @return 转账结果提示
     */
    public String transfer(User userA, User userB, BigDecimal amount) {
        // 第一层:校验转账人、收款人是否为null
        if (userA != null && userB != null) {
            // 第二层:校验转账人、收款人是否为同一人
            if (!userA.getUserId().equals(userB.getUserId())) {
                // 第三层:校验转账金额是否大于0
                if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
                    // 第四层:校验双方账户状态是否正常
                    if (User.STATUS_NORMAL.equals(userA.getStatus()) && User.STATUS_NORMAL.equals(userB.getStatus())) {
                        // 第五层:校验转账人余额是否充足
                        if (userA.getBalance().compareTo(amount) >= 0) {
                            // 所有校验通过,执行转账逻辑
                            userA.setBalance(userA.getBalance().subtract(amount));
                            userB.setBalance(userB.getBalance().add(amount));
                            return "转账成功!" + userA.getUserName() + "向" + userB.getUserName() + "转账" + amount + "元,当前余额:" + userA.getBalance();
                        } else {
                            // 余额不足失败
                            return "转账失败:转账人余额不足,当前余额:" + userA.getBalance();
                        }
                    } else {
                        // 账户状态异常失败
                        return "转账失败:账户状态异常(冻结或销户)";
                    }
                } else {
                    // 金额非法失败
                    return "转账失败:转账金额必须大于0";
                }
            } else {
                // 同一人转账失败
                return "转账失败:转账人与收款人不能为同一人";
            }
        } else {
            // 用户对象为null失败
            return "转账失败:转账人或收款人信息不能为空";
        }
    }

正例

    /**
     * 用户A向用户B转账(黄金三层级+卫语句)
     * @param userA 转账人
     * @param userB 收款人
     * @param amount 转账金额
     * @return 转账结果提示
     */
    public String transfer(User userA, User userB, BigDecimal amount) {
        // ======================================
        // 第一层:防御层(NPE防御 + 基础格式校验)
        // 目的:保护程序不崩溃,后续逻辑依赖的对象/基础格式必须先校验
        // ======================================
        // 卫语句1:防御转账人/收款人对象为null(避免NPE)
        if (userA == null) {
            return "转账失败:转账人信息不能为空(程序防御)";
        }
        if (userB == null) {
            return "转账失败:收款人信息不能为空(程序防御)";
        }
        // 卫语句2:防御转账金额对象为null(避免NPE)
        if (amount == null) {
            return "转账失败:转账金额不能为空(程序防御)";
        }
        // 卫语句3:基础格式校验(金额必须大于0,属于基础规则,非业务规则)
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            return "转账失败:转账金额必须大于0(基础格式校验)";
        }

        // ======================================
        // 第二层:业务层(前置条件 + 业务规则校验)
        // 目的:保护业务规则不被破坏,对象已安全,仅校验业务合法性
        // ======================================
        // 卫语句1:业务规则 - 转账人与收款人不能为同一人
        if (userA.getUserId().equals(userB.getUserId())) {
            return "转账失败:转账人与收款人不能为同一人(业务规则)";
        }
        // 卫语句2:业务规则 - 转账人账户状态必须正常
        if (!User.STATUS_NORMAL.equals(userA.getStatus())) {
            return String.format("转账失败:%s账户状态异常(当前状态:%s)(业务规则)", userA.getUserName(), userA.getStatus());
        }
        // 卫语句3:业务规则 - 收款人账户状态必须正常
        if (!User.STATUS_NORMAL.equals(userB.getStatus())) {
            return String.format("转账失败:%s账户状态异常(当前状态:%s)(业务规则)", userB.getUserName(), userB.getStatus());
        }
        // 卫语句4:业务规则 - 转账人余额必须充足
        if (userA.getBalance().compareTo(amount) < 0) {
            return String.format("转账失败:%s余额不足(当前余额:%s,转账金额:%s)(业务规则)", 
                    userA.getUserName(), userA.getBalance(), amount);
        }

        // ======================================
        // 第三层:核心层(执行真正的核心业务逻辑)
        // 目的:完成业务核心操作,无缩进、无校验,仅处理核心逻辑
        // 位置:方法最后,所有校验已通过,安全执行
        // ======================================
        // 核心操作1:扣减转账人余额
        userA.setBalance(userA.getBalance().subtract(amount));
        // 核心操作2:增加收款人余额
        userB.setBalance(userB.getBalance().add(amount));
        // 核心操作3(扩展):后续可添加数据库落库、发送转账通知等核心业务
        // saveTransferRecord(userA, userB, amount); // 保存转账记录(示例扩展)
        // sendTransferNotify(userA, userB, amount); // 发送转账通知(示例扩展)

        // 返回核心操作结果
        return String.format("转账成功!%s向%s转账%s元,%s当前余额:%s", 
                userA.getUserName(), userB.getUserName(), amount, userA.getUserName(), userA.getBalance());
    }

4.优点

  • Fail Fast(快速失败):有问题的请求在第一行就结束了,不会浪费系统资源去查数据库(比如查余额)
  • 零缩进:核心业务逻辑完全暴露在最外层,阅读代码的人一眼就能看到这个方法到底是干什么的,而不是在算括号匹配
  • 安全有序:因为先防住了NPE,在第二层业务检查时,我们可以放心地调用方法,完全不用担心空指针异常

5.进阶

介绍

在标准的卫语句体系中,我么遵循黄金三层级的线性结构。这非常完美,直到你遇到了非原子性的业务场景

有一种校验,既不是入参错误,也不是静态的业务前提,而是核心逻辑执行过程中的路障

比如:

  1. 依赖外部结果:必须先调用第三方风控接口,根据返回结果决定是否继续注册
  2. 依赖并发状态:扣减库存时,发现乐观锁版本号冲突(CAS失败)
  3. 依赖中间计算:必须先生成复杂的财务报表,算出总额后,发现超出了本月预算

如果处理不好,核心逻辑层会被再次撕裂成嵌套代码

解决方案

当校验无法前置时,我们将核心逻辑视为若干个步骤(Step)。每一个步骤执行完,立即跟随一个卫语句进行断言

结构演变为:

  1. 防御层
  2. 前置业务层
  3. 核心逻辑Step1
  4. 运行中卫语句(拦截Step1的结果)
  5. 核心逻辑Step2
  6. 运行中卫语句(拦截Step2的结果)
  7. 收尾

关键点:依然保持主干逻辑的零缩进,不要把后续步骤包裹在if(success)块中

示例

反例
public void registerUser (UserDTO userDto) {
    // 1.防御层 & 2.业务前提
    validateInput(UserDto);
    // 核心逻辑开始,调用外部风控服务
    boolean isBlackList = riskControlService.checkBlackList(userDto.getIdCard());
    // 糟糕的写法:把后续所有逻辑吞进去了
    if (!isBlackList) {
        User user = userMapper.toEntity(userDto);
        userRepository.save(user);
     	// 发送邮件
     	boolean emailSent = emailService.sendWelcome(user.getEmail);
     	if (!emailSent) {
     		// 甚至还有这种嵌套
            log.warn("邮件发送失败");
     	}
    } else {
        throw new BusinessException("风险用户,禁止注册");
    }
}
正例
public void registerUser (UserDTO userDto) {
    // 1.防御层 & 2.业务前提
    validateInput(UserDto);
    // 核心逻辑开始,调用外部风控服务,阶段一:外部风控检查
    boolean isBlackList = riskControlService.checkBlackList(userDto.getIdCard());
	// 检查阶段一结果,如果不满足,直接中断流程
	if (isBlackList) {
        throw new BusinessException("风险用户,禁止注册");
	}
	// 核心逻辑
	User user = userMapper.toEntity(userDto);
	userRepository.save(user);
	emailService.sendWelcome(user.getEmail);
}