重构与错误处理:用异常取代错误码,用卫语句简化错误处理
错误处理这件事,说实话,很多团队都做得不够好。
我见过太多代码里充斥着这样的场景:一个函数返回 -1 表示失败,返回 0 表示成功,返回 1 表示某种特殊错误。然后调用方要小心翼翼地检查每个返回值,稍不留神就漏掉一个判断,线上就出了事故。
今天我们就来聊聊,怎么用异常取代错误码,用卫语句简化错误处理。这两个手法,是我在项目中反复验证过的。
错误码的痛点
先看一段典型的错误码风格代码。嗯,你肯定见过:
public int saveUser(User user) {
if (user == null) {
return -1; // 参数为空
}
if (user.getName() == null || user.getName().isEmpty()) {
return -2; // 用户名不能为空
}
if (user.getAge() < 0 || user.getAge() > 150) {
return -3; // 年龄不合法
}
// 保存逻辑...
return 0; // 成功
}
调用方呢?得这样写:
int result = saveUser(user);
if (result == -1) {
// 处理参数为空
} else if (result == -2) {
// 处理用户名为空
} else if (result == -3) {
// 处理年龄不合法
} else if (result == 0) {
// 成功,继续后续逻辑
}
你想想看,这有什么问题?
- 调用方容易遗漏检查。我见过有人直接写
saveUser(user);然后继续往下走,完全不看返回值。结果数据没保存成功,用户还以为操作完成了。 - 错误码没有语义。-1、-2、-3 这些数字,鬼知道什么意思。你还要去翻文档或者看函数注释。
- 错误处理逻辑和业务逻辑混在一起。每个调用点都要写一堆 if-else,代码可读性极差。
用异常取代错误码
异常的好处是什么?说白了,它把「错误处理」和「正常逻辑」分开了。你不需要在每个调用点都检查返回值,只需要在合适的地方捕获异常即可。
重构后的代码:
public void saveUser(User user) {
if (user == null) {
throw new IllegalArgumentException("用户对象不能为空");
}
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (user.getAge() < 0 || user.getAge() > 150) {
throw new IllegalArgumentException("年龄不合法,范围应在0-150之间");
}
// 保存逻辑...
}
调用方:
try {
saveUser(user);
// 成功,继续后续逻辑
} catch (IllegalArgumentException e) {
// 统一处理参数校验错误
log.error("保存用户失败:{}", e.getMessage());
// 返回友好的错误信息给前端
}
你看,调用方只需要一个 try-catch 就能处理所有参数校验错误。而且异常本身携带了错误信息,不需要再去查错误码表。
BusinessException,然后在这个异常里带上错误码和错误信息。这样既保留了错误码的结构化信息,又享受了异常带来的便利。
卫语句:提前返回,减少嵌套
刚才的代码里,我用了多个 if 判断来校验参数。这其实已经是一种「卫语句」的写法了。但很多人写代码时,喜欢这样:
public void processOrder(Order order) {
if (order != null) {
if (order.isValid()) {
if (order.getItems() != null && !order.getItems().isEmpty()) {
// 处理订单逻辑...
} else {
log.error("订单项为空");
}
} else {
log.error("订单无效");
}
} else {
log.error("订单对象为空");
}
}
这种嵌套结构,我称之为「箭头形代码」。读起来非常累,你要一层一层地往里看,才能找到真正的业务逻辑。
用卫语句重构:
public void processOrder(Order order) {
if (order == null) {
log.error("订单对象为空");
return;
}
if (!order.isValid()) {
log.error("订单无效");
return;
}
if (order.getItems() == null || order.getItems().isEmpty()) {
log.error("订单项为空");
return;
}
// 处理订单逻辑...
}
为什么会这样?因为卫语句的核心思想是:先处理异常情况,提前返回,把正常逻辑留在最后。这样代码的阅读路径是线性的,而不是嵌套的。
异常 vs 卫语句:什么时候用哪个?
这个问题我经常被问到。我的建议是:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 参数校验、前置条件检查 | 卫语句 + 异常 | 卫语句提前返回,异常传递错误信息 |
| 业务逻辑中的错误 | 异常 | 业务错误需要被上层统一处理,不适合提前返回 |
| 外部资源调用失败 | 异常 | 网络、数据库等错误是不可预期的,必须用异常传播 |
| 简单的 null 检查 | 卫语句 | 直接 return 或抛异常,不需要 try-catch |
说白了,卫语句适合「检查到问题就立即停止」的场景,异常适合「需要把错误信息传递到上层统一处理」的场景。两者不冲突,可以结合使用。
实战案例:用户注册功能
我们来看一个完整的例子。假设有一个用户注册功能,需要校验参数、检查用户名是否重复、发送验证码。用错误码的方式写出来是这样的:
public int register(String username, String password, String email) {
if (username == null || username.length() < 3) {
return 1001; // 用户名不合法
}
if (password == null || password.length() < 6) {
return 1002; // 密码不合法
}
if (email == null || !email.contains("@")) {
return 1003; // 邮箱不合法
}
if (userRepository.existsByUsername(username)) {
return 2001; // 用户名已存在
}
// 发送验证码...
if (!smsService.sendCode(email)) {
return 3001; // 验证码发送失败
}
// 保存用户...
return 0; // 成功
}
调用方:
int result = register(username, password, email);
switch (result) {
case 1001: // 提示用户名不合法
case 1002: // 提示密码不合法
case 1003: // 提示邮箱不合法
case 2001: // 提示用户名已存在
case 3001: // 提示验证码发送失败
case 0: // 注册成功,跳转页面
}
重构后,用异常 + 卫语句:
public void register(String username, String password, String email) {
// 卫语句:参数校验
if (username == null || username.length() < 3) {
throw new BusinessException(1001, "用户名长度至少3位");
}
if (password == null || password.length() < 6) {
throw new BusinessException(1002, "密码长度至少6位");
}
if (email == null || !email.contains("@")) {
throw new BusinessException(1003, "邮箱格式不正确");
}
// 卫语句:业务校验
if (userRepository.existsByUsername(username)) {
throw new BusinessException(2001, "用户名已被注册");
}
// 发送验证码
boolean sent = smsService.sendCode(email);
if (!sent) {
throw new BusinessException(3001, "验证码发送失败,请稍后重试");
}
// 保存用户
userRepository.save(new User(username, password, email));
}
调用方:
try {
register(username, password, email);
// 注册成功,跳转页面
} catch (BusinessException e) {
// 统一处理业务错误,e.getCode() 和 e.getMessage() 返回给前端
log.warn("注册失败:{}", e.getMessage());
// 显示错误信息给用户
}
你看,调用方只需要一个 try-catch,所有业务错误都被统一处理了。而且每个错误都带有明确的错误码和错误信息,前端可以根据错误码做不同的展示。
SVG:错误处理重构的核心逻辑
下面这张图展示了从错误码到异常 + 卫语句的转变过程:
避坑指南
最后,分享几个我踩过的坑:
- 不要用异常控制流程。比如用 try-catch 来跳出循环,或者用异常来传递正常业务数据。异常的开销比普通返回大得多,而且会让代码逻辑变得混乱。
- 不要吞掉异常。我见过有人写
catch (Exception e) {},什么都不做。这等于把错误藏起来了,线上出了问题根本查不到原因。至少打个日志。 - 卫语句不要过度。如果一个函数有七八个卫语句,说明这个函数做的事情太多了。考虑拆分成多个小函数。
- 异常粒度要适中。不要一个函数只抛一种异常,也不要抛几十种。我个人习惯是:每个模块定义 3-5 种业务异常,配合错误码使用。
Exception,然后调用方用 e.getMessage() 来判断错误类型。结果后来需求变了,错误信息改了,前端解析就全乱了。从那以后,我坚持用自定义异常 + 错误码,前端只认错误码,不认错误信息。
好了,关于用异常取代错误码、用卫语句简化错误处理,就聊到这里。这两个手法结合起来,能让你的错误处理代码变得干净、可维护。下次写代码时,不妨试试看。
公众号:蓝海资料掘金营,微信deep3321