25、函数设计(下):使用异常替代返回错误码、抽离Try/Catch代码块、别重复自己
上一章我们聊了函数要短、要小、要做一件事。今天继续往下挖。说白了,函数设计的下半场,核心就三个字:别糊弄。
错误处理这件事,我见过太多人把它当成“补丁”来写。函数里先写业务逻辑,然后在某个角落塞个 if-else 返回 -1 或者 null。嗯,这种代码我年轻时也写过。后来被坑了几次,才明白——错误处理不是附属品,它本身就是函数设计的一部分。
用异常替代返回错误码
先看一个反面教材。这是我早期项目里真实出现过的代码:
// 错误码风格
public int saveUser(User user) {
if (user == null) {
return -1; // 用户为空
}
if (user.getName() == null || user.getName().isEmpty()) {
return -2; // 名字不合法
}
// 保存逻辑...
return 0; // 成功
}
调用方呢?更惨:
int result = saveUser(user);
if (result == -1) {
// 处理空用户
} else if (result == -2) {
// 处理名字问题
} else if (result == 0) {
// 正常流程
}
你想想看,这代码读起来像什么?像在查一本错误码字典。而且最要命的是——调用方很容易忘记检查返回值。我踩过这个坑,有一次线上数据丢了,查了半天,发现就是某个函数返回了 -3,但调用方直接忽略了,继续往下执行。
正确的做法是什么?用异常:
public void saveUser(User user) {
if (user == null) {
throw new IllegalArgumentException("用户对象不能为空");
}
if (user.getName() == null || user.getName().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
// 保存逻辑...
}
调用方只需要在合适的地方 catch 一次:
try {
userService.saveUser(user);
} catch (IllegalArgumentException e) {
// 统一处理参数错误
logger.warn("保存用户参数异常", e);
return errorResponse(e.getMessage());
}
好处很明显:主流程和错误处理分开了。代码不再被 if-else 割得支离破碎。我个人习惯是,业务异常用自定义异常类,系统异常用 RuntimeException 的子类。这样 catch 的时候能精确区分。
抽离 Try/Catch 代码块
好,现在你开始用异常了。但别高兴太早——很多人又掉进另一个坑:把 try/catch 写得满屏都是。
来看这个:
public void processOrder(Order order) {
try {
// 验证订单
validateOrder(order);
// 计算价格
BigDecimal price = calculatePrice(order);
// 扣减库存
deductStock(order);
// 发送通知
sendNotification(order);
} catch (ValidationException e) {
logger.error("订单验证失败", e);
throw e;
} catch (PriceException e) {
logger.error("价格计算异常", e);
throw e;
} catch (StockException e) {
logger.error("库存扣减失败", e);
throw e;
}
}
这代码有什么问题?try 块里塞了太多事。你想想看,如果 validateOrder 抛异常,后面的 calculatePrice 根本不会执行。但读代码的人需要从头到尾看完整个 try 块,才能理解“哦,原来这些步骤是顺序执行的”。
我建议的做法是:把 try/catch 抽成一个单独的函数。
public void processOrder(Order order) {
try {
doProcessOrder(order);
} catch (ValidationException | PriceException | StockException e) {
logger.error("订单处理失败", e);
throw new OrderProcessException("处理订单异常", e);
}
}
private void doProcessOrder(Order order) {
validateOrder(order);
BigDecimal price = calculatePrice(order);
deductStock(order);
sendNotification(order);
}
看到了吗?doProcessOrder 只做业务逻辑,没有错误处理的干扰。而 processOrder 只负责错误处理,不关心具体业务。这就是单一职责原则在错误处理上的体现。
别重复自己(DRY 原则)
这个原则说起来简单,做起来难。我见过最夸张的一个项目,同一个“用户不存在”的错误处理逻辑,在 12 个地方重复出现。后来需求变了,要改成“用户不存在时发送短信提醒”,改代码的人差点崩溃。
重复的错误处理长什么样?
// Controller A
try {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.status(404).body("用户不存在");
}
// ...
} catch (Exception e) {
return ResponseEntity.status(500).body("系统错误");
}
// Controller B
try {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.status(404).body("用户不存在");
}
// ...
} catch (Exception e) {
return ResponseEntity.status(500).body("系统错误");
}
这代码有什么问题?一模一样的逻辑写了两次。如果哪天要把 404 改成 400,你得改两个地方。万一漏了一个呢?
正确的做法是:把重复的错误处理逻辑抽象成公共方法或全局异常处理器。
// 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException e) {
return ResponseEntity.status(404).body(e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralError(Exception e) {
logger.error("系统异常", e);
return ResponseEntity.status(500).body("系统繁忙,请稍后重试");
}
}
这样,每个 Controller 只需要抛出对应的异常,错误处理由全局统一接管。代码量减少,维护成本降低,而且错误处理的行为是一致的。
核心要点:
- 用异常替代返回码,让主流程更干净
- 抽离 try/catch 块,让错误处理和业务逻辑分离
- 消除重复的错误处理代码,用统一机制管理
本章知识体系
下面这张图总结了函数设计中错误处理的三个核心原则,以及它们之间的关系:
这三个原则其实是递进关系。先用异常替代返回码,让错误能被“抛出”而不是“返回”;然后把 try/catch 抽离出来,让错误处理不污染业务逻辑;最后用 DRY 原则消除重复,让整个系统的错误处理风格统一。
我个人觉得,错误处理是衡量一个工程师是否成熟的重要标志。新手往往只关注“正常流程怎么走”,而老手会花同样多的精力思考“出错了怎么办”。
嗯,今天就到这里。记住:好的代码,连错误处理都是优雅的。