第8章 防御式编程
一、导读
1.1 本章学习目标
第8章介绍了防御式编程的理念和实践。McConnell 指出,健壮的程序不是偶然产生的,而是通过防御式编程技术刻意构建的。本章的核心学习目标包括:
- 理解防御式编程的本质:不信任任何外部输入,验证所有假设。
- 掌握断言的使用方法:验证程序内部假设的正确性。
- 学会错误处理的最佳实践:优雅地处理可预见的错误。
- 掌握异常处理的技术:使用异常机制处理错误情况。
- 理解防御性复制的重要性:保护对象不被意外修改。
- 学会输入验证的策略:不信任外部输入。
1.2 为什么本章如此重要
McConnell 指出:软件缺陷不是偶然发生的,而是必然发生的。无论程序员多么优秀,错误总是会出现。防御式编程的核心思想是:假设错误一定会发生,提前做好准备。
本章的重要性在于它提供了一套系统化的防御技术。你会理解如何预见可能的错误,如何在错误发生时保护系统和数据,以及如何提供有意义的错误信息。
1.3 核心问题引导
什么是防御式编程?它与"乐观编程"有什么区别?
断言应该在什么情况下使用?什么情况下不应该使用?
错误处理和异常处理有什么区别?如何选择?
为什么不能信任外部输入?如何进行输入验证?
防御性复制的目的是什么?在什么情况下需要?
二、核心概念详解
2.1 断言
2.1.1 断言的本质
断言用于验证程序内部的假设。它表达的是:"如果这个条件不成立,说明程序有 bug"。
// 断言:验证内部假设
public void processOrder(Order order) {
// 断言:order 不应该为 null
// 如果为 null,说明调用方有 bug
assert order != null : "Order must not be null";
// 断言:订单必须有商品
assert order.getItems().size() > 0 : "Order must have items";
// 断言:订单状态应该是 CREATED
assert order.getStatus() == OrderStatus.CREATED
: "Order should be in CREATED status, but was: " + order.getStatus();
// 处理订单...
}2.1.2 断言的使用原则
- 断言内部条件:不用于验证外部输入
- 断言不可能为假的条件:如果条件可能为假,应该用 if 处理
- 断言应该附带错误信息:帮助定位问题
// 正确:断言内部条件
public void withdraw(Account account, Money amount) {
assert account != null : "Account must not be null";
assert amount.isPositive() : "Amount must be positive";
// 这是内部逻辑保证的
assert account.getBalance().greaterThanOrEqual(amount)
: "Insufficient balance: " + account.getBalance();
account.debit(amount);
}
// 错误:用断言验证外部输入
public void processInput(String input) {
// 错!input 来自外部,可能为 null
assert input != null;
// 应该用 if 处理
if (input == null) {
throw new IllegalArgumentException("Input must not be null");
}
}2.1.3 断言与异常的区别
| 特性 | 断言 | 异常 |
|---|---|---|
| 用途 | 验证内部假设 | 处理外部错误 |
| 触发条件 | 程序 bug | 可预见的错误情况 |
| 是否可恢复 | 不可恢复 | 可以恢复 |
| 生产环境 | 通常关闭 | 始终开启 |
2.2 错误处理
2.2.1 错误处理的层次
McConnell 将错误处理分为三个层次:
输入验证:在进入业务逻辑前检查输入
业务规则验证:检查业务规则是否满足
系统错误处理:处理系统级错误(网络、数据库等)
public OrderResult createOrder(CreateOrderRequest request) {
// 层次1:输入验证
if (request == null) {
return OrderResult.error("Request must not be null");
}
if (request.getCustomerId() == null) {
return OrderResult.error("Customer ID is required");
}
if (request.getItems().isEmpty()) {
return OrderResult.error("Order must have at least one item");
}
// 层次2:业务规则验证
Customer customer = customerRepository.findById(request.getCustomerId());
if (customer == null) {
return OrderResult.error("Customer not found");
}
if (customer.isSuspended()) {
return OrderResult.error("Customer account is suspended");
}
for (OrderItemRequest item : request.getItems()) {
Product product = productRepository.findById(item.getProductId());
if (product == null) {
return OrderResult.error("Product not found: " + item.getProductId());
}
if (product.getStock() < item.getQuantity()) {
return OrderResult.error("Insufficient stock for: " + product.getName());
}
}
// 层次3:系统错误处理
try {
Order order = buildOrder(request);
orderRepository.save(order);
return OrderResult.success(order.getId());
} catch (DatabaseException e) {
logger.error("Database error creating order", e);
return OrderResult.error("System error, please try again later");
}
}2.2.2 错误处理的策略
- 在最近的层次处理错误:不要将错误传播到不必要的层次
- 提供有意义的错误信息:帮助调用者理解问题
- 区分可恢复和不可恢复的错误:可恢复的返回结果,不可恢复的抛异常
// 好的错误处理
public class PaymentService {
public PaymentResult charge(CustomerId customerId, Money amount) {
// 输入验证
if (amount.isNegative()) {
return PaymentResult.error("Amount must be positive");
}
// 业务验证
Customer customer = customerRepository.findById(customerId);
if (customer == null) {
return PaymentResult.error("Customer not found");
}
// 系统错误处理
try {
PaymentGateway gateway = getGateway(customer.getPreferredGateway());
GatewayResponse response = gateway.charge(customer, amount);
if (response.isSuccess()) {
return PaymentResult.success(response.getTransactionId());
} else {
// 可恢复的错误:返回结果
return PaymentResult.error(response.getErrorMessage());
}
} catch (NetworkException e) {
// 可恢复的系统错误:重试
return retryCharge(customerId, amount);
} catch (GatewayException e) {
// 不可恢复的系统错误:记录日志,返回通用错误
logger.error("Payment gateway error", e);
return PaymentResult.error("Payment system unavailable");
}
}
}2.3 异常处理
2.3.1 异常处理的原则
- 只捕获能处理的异常:不要捕获异常然后忽略
- 使用具体的异常类型:不要捕获 Exception 或 Throwable
- 提供异常上下文:包含足够的调试信息
// 差:捕获所有异常并忽略
try {
processOrder(order);
} catch (Exception e) {
// 什么都不做,错误被吞没
}
// 差:捕获过于宽泛
try {
processOrder(order);
} catch (Exception e) {
e.printStackTrace(); // 只是打印,没有真正处理
}
// 好:捕获具体异常并提供上下文
try {
processOrder(order);
} catch (OrderValidationException e) {
// 可恢复:返回验证错误
return OrderResult.validationError(e.getErrors());
} catch (InsufficientStockException e) {
// 可恢复:返回库存不足
return OrderResult.insufficientStock(e.getProductId());
} catch (PaymentGatewayException e) {
// 不可恢复:记录日志,返回系统错误
logger.error("Payment gateway failed for order {}: {}",
order.getId(), e.getMessage(), e);
return OrderResult.systemError("Payment system unavailable");
}2.3.2 自定义异常
// 自定义异常:提供业务上下文
public class OrderException extends RuntimeException {
private final OrderId orderId;
private final ErrorCode errorCode;
public OrderException(OrderId orderId, ErrorCode errorCode, String message) {
super(message);
this.orderId = orderId;
this.errorCode = errorCode;
}
public OrderException(OrderId orderId, ErrorCode errorCode,
String message, Throwable cause) {
super(message, cause);
this.orderId = orderId;
this.errorCode = errorCode;
}
public OrderId getOrderId() { return orderId; }
public ErrorCode getErrorCode() { return errorCode; }
}
// 使用自定义异常
public class OrderService {
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new OrderException(orderId, ErrorCode.ORDER_NOT_FOUND,
"Order not found: " + orderId);
}
if (order.getStatus() != OrderStatus.CREATED) {
throw new OrderException(orderId, ErrorCode.INVALID_STATUS,
"Cannot confirm order in status: " + order.getStatus());
}
try {
order.confirm();
orderRepository.save(order);
} catch (DatabaseException e) {
throw new OrderException(orderId, ErrorCode.SYSTEM_ERROR,
"Failed to save order", e);
}
}
}2.4 防御性复制
2.4.1 为什么需要防御性复制
当对象被传递给外部代码时,如果不做保护,外部代码可能修改对象的内部状态。
// 差:暴露内部可变对象
public class User {
private List<String> roles;
public List<String> getRoles() {
return roles; // 返回原始引用,外部可以修改
}
}
// 外部代码可以修改内部状态
User user = new User();
List<String> roles = user.getRoles();
roles.add("ADMIN"); // 直接修改了 user 的内部状态!
// 好:防御性复制
public class User {
private final List<String> roles;
public User(List<String> roles) {
// 构造时复制
this.roles = new ArrayList<>(roles);
}
public List<String> getRoles() {
// 返回不可变视图
return Collections.unmodifiableList(roles);
}
public void addRole(String role) {
// 通过方法修改,保持控制
roles.add(role);
}
}2.4.2 防御性复制的场景
// 场景1:构造函数中复制
public class Order {
private final List<OrderItem> items;
public Order(List<OrderItem> items) {
this.items = new ArrayList<>(items); // 防御性复制
}
}
// 场景2:getter 中复制
public class Order {
private final List<OrderItem> items;
public List<OrderItem> getItems() {
return new ArrayList<>(items); // 返回副本
// 或者返回不可变视图
// return Collections.unmodifiableList(items);
}
}
// 场景3:方法参数复制
public void processItems(List<OrderItem> items) {
List<OrderItem> safeCopy = new ArrayList<>(items);
// 使用 safeCopy 而非 items
}2.5 输入验证
2.5.1 输入验证的原则
- 不信任外部输入:所有来自外部的数据都应该验证
- 尽早验证:在数据进入系统时就验证
- 验证所有属性:类型、范围、格式、业务规则
public class InputValidator {
// 验证用户注册输入
public ValidationResult validateRegistration(RegistrationRequest request) {
List<String> errors = new ArrayList<>();
// 验证用户名
if (request.getUsername() == null || request.getUsername().isEmpty()) {
errors.add("Username is required");
} else if (request.getUsername().length() < 3) {
errors.add("Username must be at least 3 characters");
} else if (request.getUsername().length() > 50) {
errors.add("Username must be at most 50 characters");
} else if (!request.getUsername().matches("^[a-zA-Z0-9_]+$")) {
errors.add("Username can only contain letters, numbers and underscores");
}
// 验证邮箱
if (request.getEmail() == null || request.getEmail().isEmpty()) {
errors.add("Email is required");
} else if (!isValidEmail(request.getEmail())) {
errors.add("Invalid email format");
}
// 验证密码
if (request.getPassword() == null || request.getPassword().isEmpty()) {
errors.add("Password is required");
} else if (request.getPassword().length() < 8) {
errors.add("Password must be at least 8 characters");
} else if (!hasUpperCase(request.getPassword())) {
errors.add("Password must contain at least one uppercase letter");
} else if (!hasDigit(request.getPassword())) {
errors.add("Password must contain at least one digit");
}
return new ValidationResult(errors);
}
private boolean isValidEmail(String email) {
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
private boolean hasUpperCase(String str) {
return !str.equals(str.toLowerCase());
}
private boolean hasDigit(String str) {
return str.chars().anyMatch(Character::isDigit);
}
}2.5.2 使用注解验证
// 使用 JSR 303 Bean Validation
public class CreateUserRequest {
@NotNull(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be 3-50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Invalid username format")
private String username;
@NotNull(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotNull(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
}
// 在控制器中自动验证
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
// 验证失败会自动返回 400 错误
userService.createUser(request);
return ResponseEntity.ok().build();
}
}三、实战案例
3.1 案例一:完整的防御式编程实践
public class TransferService {
public TransferResult transferMoney(TransferRequest request) {
// 1. 断言:验证内部假设
assert request != null : "TransferRequest must not be null";
// 2. 输入验证:不信任外部输入
if (request.getFromAccountId() == null) {
return TransferResult.error("Source account ID is required");
}
if (request.getToAccountId() == null) {
return TransferResult.error("Target account ID is required");
}
if (request.getAmount() == null || request.getAmount().isNegative()) {
return TransferResult.error("Transfer amount must be positive");
}
if (request.getFromAccountId().equals(request.getToAccountId())) {
return TransferResult.error("Cannot transfer to the same account");
}
// 3. 业务验证
BankAccount fromAccount = accountRepository.findById(request.getFromAccountId());
if (fromAccount == null) {
return TransferResult.error("Source account not found");
}
BankAccount toAccount = accountRepository.findById(request.getToAccountId());
if (toAccount == null) {
return TransferResult.error("Target account not found");
}
if (fromAccount.isFrozen()) {
return TransferResult.error("Source account is frozen");
}
if (fromAccount.getBalance().lessThan(request.getAmount())) {
return TransferResult.error("Insufficient balance");
}
// 4. 防御性复制
List<TransferDetail> details = new ArrayList<>(request.getDetails());
// 5. 系统错误处理
try {
fromAccount.debit(request.getAmount());
toAccount.credit(request.getAmount());
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
recordTransaction(request, fromAccount, toAccount);
return TransferResult.success(generateTransactionId());
} catch (DatabaseException e) {
logger.error("Database error during transfer from {} to {}: {}",
request.getFromAccountId(), request.getToAccountId(),
e.getMessage(), e);
return TransferResult.error("System error, please try again later");
} catch (Exception e) {
logger.error("Unexpected error during transfer", e);
return TransferResult.error("Unexpected error occurred");
}
}
}四、本章小结
第8章讲解了防御式编程的理念和实践,核心要点包括:
防御式编程的本质:假设错误一定会发生,提前做好准备。
断言验证内部假设:用于检测程序 bug,不用于验证外部输入。
错误处理要分层:输入验证、业务验证、系统错误处理。
异常处理要具体:只捕获能处理的异常,提供上下文信息。
防御性复制保护数据:防止外部代码修改内部状态。
输入验证不信任外部:所有外部输入都应该验证。
McConnell 的核心观点是:健壮的程序不是偶然的,而是通过防御式编程技术刻意构建的。
五、思考与练习
思考题
你的代码中有断言吗?断言的使用是否合理?
你的错误处理策略是什么?是否有统一的错误处理机制?
你的代码中有防御性复制吗?哪些地方需要但没有做?
你的输入验证是否充分?有没有遗漏的验证?
编程练习
为一个"用户登录"功能编写完整的防御式编程代码,包括断言、输入验证、错误处理。
找到一个暴露内部可变对象的类,添加防御性复制。
设计一个自定义异常体系,为一个业务模块提供有意义的异常信息。