卫语句代码规范
1.介绍
在传统的代码编写中,存在一种很恶心代码结构:
使用卫语句可以优化这种代码结构
卫语句的核心思想就是:如果不符合条件,立刻滚蛋(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.进阶
介绍
在标准的卫语句体系中,我么遵循黄金三层级的线性结构。这非常完美,直到你遇到了非原子性的业务场景
有一种校验,既不是入参错误,也不是静态的业务前提,而是核心逻辑执行过程中的路障
比如:
- 依赖外部结果:必须先调用第三方风控接口,根据返回结果决定是否继续注册
- 依赖并发状态:扣减库存时,发现乐观锁版本号冲突(CAS失败)
- 依赖中间计算:必须先生成复杂的财务报表,算出总额后,发现超出了本月预算
如果处理不好,核心逻辑层会被再次撕裂成嵌套代码
解决方案
当校验无法前置时,我们将核心逻辑视为若干个步骤(Step)。每一个步骤执行完,立即跟随一个卫语句进行断言
结构演变为:
- 防御层
- 前置业务层
- 核心逻辑Step1
- 运行中卫语句(拦截Step1的结果)
- 核心逻辑Step2
- 运行中卫语句(拦截Step2的结果)
- 收尾
关键点:依然保持主干逻辑的零缩进,不要把后续步骤包裹在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);
}