8代码大全(第2版)

防御式编程

安全编程

防御式编程断言错误处理

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

第8章 防御式编程

一、导读

1.1 本章学习目标

第8章介绍了防御式编程的理念和实践。McConnell 指出,健壮的程序不是偶然产生的,而是通过防御式编程技术刻意构建的。本章的核心学习目标包括:

  • 理解防御式编程的本质:不信任任何外部输入,验证所有假设。
  • 掌握断言的使用方法:验证程序内部假设的正确性。
  • 学会错误处理的最佳实践:优雅地处理可预见的错误。
  • 掌握异常处理的技术:使用异常机制处理错误情况。
  • 理解防御性复制的重要性:保护对象不被意外修改。
  • 学会输入验证的策略:不信任外部输入。

1.2 为什么本章如此重要

McConnell 指出:软件缺陷不是偶然发生的,而是必然发生的。无论程序员多么优秀,错误总是会出现。防御式编程的核心思想是:假设错误一定会发生,提前做好准备。

本章的重要性在于它提供了一套系统化的防御技术。你会理解如何预见可能的错误,如何在错误发生时保护系统和数据,以及如何提供有意义的错误信息。

1.3 核心问题引导

什么是防御式编程?它与"乐观编程"有什么区别?

断言应该在什么情况下使用?什么情况下不应该使用?

错误处理和异常处理有什么区别?如何选择?

为什么不能信任外部输入?如何进行输入验证?

防御性复制的目的是什么?在什么情况下需要?


二、核心概念详解

2.1 断言

2.1.1 断言的本质

断言用于验证程序内部的假设。它表达的是:"如果这个条件不成立,说明程序有 bug"。

java
// 断言:验证内部假设
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 处理
  • 断言应该附带错误信息:帮助定位问题
java
// 正确:断言内部条件
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 将错误处理分为三个层次:

输入验证:在进入业务逻辑前检查输入

业务规则验证:检查业务规则是否满足

系统错误处理:处理系统级错误(网络、数据库等)

java
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 错误处理的策略

  • 在最近的层次处理错误:不要将错误传播到不必要的层次
  • 提供有意义的错误信息:帮助调用者理解问题
  • 区分可恢复和不可恢复的错误:可恢复的返回结果,不可恢复的抛异常
java
// 好的错误处理
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
  • 提供异常上下文:包含足够的调试信息
java
// 差:捕获所有异常并忽略
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 自定义异常

java
// 自定义异常:提供业务上下文
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 为什么需要防御性复制

当对象被传递给外部代码时,如果不做保护,外部代码可能修改对象的内部状态。

java
// 差:暴露内部可变对象
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 防御性复制的场景

java
// 场景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 输入验证的原则

  • 不信任外部输入:所有来自外部的数据都应该验证
  • 尽早验证:在数据进入系统时就验证
  • 验证所有属性:类型、范围、格式、业务规则
java
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 使用注解验证

java
// 使用 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 案例一:完整的防御式编程实践

java
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 的核心观点是:健壮的程序不是偶然的,而是通过防御式编程技术刻意构建的。


五、思考与练习

思考题

你的代码中有断言吗?断言的使用是否合理?

你的错误处理策略是什么?是否有统一的错误处理机制?

你的代码中有防御性复制吗?哪些地方需要但没有做?

你的输入验证是否充分?有没有遗漏的验证?

编程练习

为一个"用户登录"功能编写完整的防御式编程代码,包括断言、输入验证、错误处理。

找到一个暴露内部可变对象的类,添加防御性复制。

设计一个自定义异常体系,为一个业务模块提供有意义的异常信息。

8/12防御式编
0%