7代码整洁之道

错误处理

异常处理

异常错误码不返回null

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第7章 错误处理

一、导读

1.1 本章学习目标

第7章讲解了错误处理的最佳实践。Uncle Bob 强调,错误处理很重要,但如果它掩盖了逻辑,那就错了。本章的核心学习目标包括:

  • 掌握使用异常替代错误码:异常比错误码更清晰。
  • 学会先写 try-catch-finally:从异常开始设计。
  • 理解提供异常上下文:包含足够的调试信息。
  • 掌握使用 unchecked 异常:减少代码噪音。
  • 学会定义异常类:提供调用者需要的上下文。
  • 掌握不返回 null,不传递 null:避免 NullPointerException。

1.2 为什么本章如此重要

错误处理是代码质量的关键指标。Uncle Bob 指出:错误处理很重要,但如果它掩盖了逻辑,那就错了。好的错误处理应该清晰、简洁,不影响主逻辑的可读性。

1.3 核心问题引导

你的代码用错误码还是异常处理错误?

异常信息是否包含足够的上下文?

你的方法返回 null 吗?这导致了什么问题?

如何设计异常类?

unchecked 异常和 checked 异常如何选择?


二、核心概念详解

2.1 使用异常替代错误码

2.1.1 错误码的问题

java
// 差:返回错误码
public class DeviceController {
  public int sendShutdown(int deviceId) {
    if (deviceId < 0) {
      return -1; // 错误码:无效设备
    }
    if (!deviceExists(deviceId)) {
      return -2; // 错误码:设备不存在
    }
    // 发送关闭命令
    return 0; // 成功
  }
}

// 调用者必须检查返回值
int result = controller.sendShutdown(1);
if (result == -1) {
  // 处理无效设备
} else if (result == -2) {
  // 处理设备不存在
}
// 容易忘记检查!

2.1.2 异常的优势

java
// 好:使用异常
public class DeviceController {
  public void sendShutdown(int deviceId) {
    if (deviceId < 0) {
      throw new IllegalArgumentException("Device ID must be positive");
    }
    if (!deviceExists(deviceId)) {
      throw new DeviceNotFoundException("Device not found: " + deviceId);
    }
    // 发送关闭命令
  }
}

// 调用者无法忽略异常
try {
  controller.sendShutdown(1);
} catch (DeviceNotFoundException e) {
  // 处理设备不存在
}

2.2 先写 try-catch-finally

2.2.1 从异常开始设计

当你写可能抛出异常的代码时,先写 try-catch-finally,再写业务逻辑。

java
// 从异常开始设计
public class StorageService {
  
  public void saveFile(String content, String fileName) {
    try {
      // 先写 try 块
      FileOutputStream fos = new FileOutputStream(fileName);
      fos.write(content.getBytes());
      fos.close();
    } catch (FileNotFoundException e) {
      // 再写 catch 块
      logger.error("File not found: {}", fileName, e);
      throw new StorageException("Cannot save file: " + fileName, e);
    } catch (IOException e) {
      logger.error("IO error saving file: {}", fileName, e);
      throw new StorageException("IO error: " + fileName, e);
    } finally {
      // 最后写 finally 块
      // 确保资源释放
    }
  }
}

2.3 提供异常上下文

2.3.1 包含足够的信息

java
// 差:异常信息不足
try {
  // ...
} catch (Exception e) {
  throw new RuntimeException("Error"); // 什么错误?在哪里?
}

// 好:包含上下文
try {
  processOrder(order);
} catch (PaymentException e) {
  throw new OrderProcessingException(
      "Failed to process order " + order.getId() + 
      " for customer " + order.getCustomerId() + 
      ": " + e.getMessage(), e);
}

2.4 使用 unchecked 异常

2.4.1 checked vs unchecked

java
// checked 异常:强制处理,但增加代码噪音
public void readFile(String path) throws IOException {
  // 每个调用者都必须处理 IOException
}

// unchecked 异常:不强制处理,减少噪音
public void readFile(String path) {
  try {
    // 读取文件
  } catch (IOException e) {
    throw new UncheckedIOException("Cannot read file: " + path, e);
  }
}

2.5 不返回 null

2.5.1 null 的问题

java
// 差:返回 null
public List<Order> getOrders(CustomerId customerId) {
  List<Order> orders = orderRepository.findByCustomerId(customerId);
  if (orders == null) {
    return null; // 调用者必须检查 null
  }
  return orders;
}

// 调用者
List<Order> orders = service.getOrders(customerId);
if (orders != null) { // 容易忘记检查!
  for (Order order : orders) {
    // ...
  }
}

// 好:不返回 null
public List<Order> getOrders(CustomerId customerId) {
  List<Order> orders = orderRepository.findByCustomerId(customerId);
  return orders != null ? orders : Collections.emptyList();
}

// 调用者不需要检查 null
List<Order> orders = service.getOrders(customerId);
for (Order order : orders) { // 安全,空列表不会进入循环
  // ...
}

2.5.2 使用 Optional

java
// 使用 Optional 替代 null
public Optional<User> findUser(UserId userId) {
  User user = userRepository.findById(userId);
  return Optional.ofNullable(user);
}

// 调用者
Optional<User> user = service.findUser(userId);
user.ifPresent(u -> {
  // 处理用户
});

// 或者
User user = service.findUser(userId)
    .orElseThrow(() -> new UserNotFoundException(userId));

三、实战案例

3.1 案例一:完整的错误处理

java
// 改善前:混乱的错误处理
public String processOrder(Order order) {
  if (order == null) return "error: null order";
  if (order.getItems().isEmpty()) return "error: no items";
  
  try {
    double total = 0;
    for (Item item : order.getItems()) {
      total += item.getPrice() * item.getQuantity();
    }
    // 保存
    database.save(order);
    return "success";
  } catch (Exception e) {
    return "error: " + e.getMessage();
  }
}

// 改善后:清晰的错误处理
public class OrderService {
  
  public OrderResult processOrder(Order order) {
    validateOrder(order);
    
    try {
      Money total = calculateTotal(order);
      order.setTotal(total);
      orderRepository.save(order);
      return OrderResult.success(order.getId());
    } catch (DatabaseException e) {
      logger.error("Failed to save order {}: {}", 
                   order.getId(), e.getMessage(), e);
      return OrderResult.systemError("Failed to save order");
    }
  }
  
  private void validateOrder(Order order) {
    if (order == null) {
      throw new IllegalArgumentException("Order must not be null");
    }
    if (order.getItems().isEmpty()) {
      throw new IllegalArgumentException("Order must have items");
    }
  }
}

// 结果对象
public class OrderResult {
  private final boolean success;
  private final OrderId orderId;
  private final String errorMessage;
  
  public static OrderResult success(OrderId orderId) {
    return new OrderResult(true, orderId, null);
  }
  
  public static OrderResult systemError(String message) {
    return new OrderResult(false, null, message);
  }
}

四、本章小结

第7章讲解了错误处理的最佳实践,核心要点包括:

使用异常替代错误码:异常更清晰,无法被忽略。

先写 try-catch-finally:从异常开始设计。

提供异常上下文:包含足够的调试信息。

使用 unchecked 异常:减少代码噪音。

不返回 null:使用空集合或 Optional。

不传递 null:在方法入口验证参数。

Uncle Bob 的核心观点是:错误处理应该清晰、简洁,不影响主逻辑的可读性。


五、思考与练习

思考题

你的代码用错误码还是异常?

异常信息是否包含足够的上下文?

你的方法返回 null 吗?如何改善?

编程练习

将一段使用错误码的代码改造为使用异常。

为一个方法添加完整的异常处理。

找到所有返回 null 的方法,使用 Optional 或空集合替代。

7/10错误处理
0%