7代码大全(第2版)

高质量的子程序

函数设计

函数参数返回值

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

第7章 高质量的子程序

一、导读

1.1 本章学习目标

第7章讲解了如何创建高质量的子程序(函数/方法)。McConnell 强调,函数是代码的基本构建块,其质量直接决定了整体代码质量。本章的核心学习目标包括:

  • 掌握函数设计的核心原则:短小精悍,专注单一职责。
  • 理解功能内聚的重要性:一个函数只做一件事,做好它。
  • 学会合理的参数设计:数量适中,语义明确。
  • 掌握返回值设计的技巧:清晰表达函数的结果。
  • 理解函数命名的艺术:动词+名词,表达意图。
  • 避免副作用:函数应该是纯函数。

1.2 为什么本章如此重要

函数是代码的最小可复用单元。McConnell 指出:如果你能写好函数,你就能写好代码。函数的质量直接决定了代码的可读性、可测试性和可维护性。

本章的重要性在于它提供了函数设计的系统化方法。你会理解为什么有些函数清晰易懂,有些混乱难懂,以及如何写出高质量的函数。

1.3 核心问题引导

函数的理想长度是多少?如何判断一个函数是否太长?

什么是"单一职责"?一个函数如何做到"只做一件事"?

函数参数应该有多少个?参数过多时如何处理?

为什么副作用是危险的?如何编写纯函数?

好的函数命名应该遵循什么规则?


二、核心概念详解

2.1 函数的长度

2.1.1 短小精悍

McConnell 引用研究数据表明:函数的理想长度在 7-10 行之间,最长不超过 50 行。过长的函数难以理解、难以测试、难以维护。

java
// 差:过长的函数(做了太多事)
public void processOrder(Order order) {
  // 验证订单(20行代码)
  if (order == null) throw new IllegalArgumentException();
  if (order.getItems().isEmpty()) throw new IllegalArgumentException();
  for (OrderItem item : order.getItems()) {
    if (item.getPrice() < 0) throw new IllegalArgumentException();
    if (item.getQuantity() <= 0) throw new IllegalArgumentException();
  }
  // ... 更多验证
  
  // 计算价格(15行代码)
  double subtotal = 0;
  for (OrderItem item : order.getItems()) {
    subtotal += item.getPrice() * item.getQuantity();
  }
  double discount = 0;
  if (order.hasCoupon()) {
    discount = subtotal * 0.1;
  }
  double tax = (subtotal - discount) * 0.08;
  double total = subtotal - discount + tax;
  
  // 保存订单(10行代码)
  order.setTotal(total);
  order.setStatus("CONFIRMED");
  database.save(order);
  
  // 发送通知(10行代码)
  Email email = new Email();
  email.setTo(order.getCustomer().getEmail());
  email.setSubject("Order Confirmation");
  email.setBody("Your order total: $" + total);
  email.send();
}

// 好:每个函数短小精悍
public void processOrder(Order order) {
  validateOrder(order);
  Money total = calculateTotal(order);
  order.setTotal(total);
  saveOrder(order);
  sendConfirmation(order);
}

private void validateOrder(Order order) {
  Objects.requireNonNull(order, "Order cannot be null");
  if (order.getItems().isEmpty()) {
    throw new IllegalArgumentException("Order must have items");
  }
  for (OrderItem item : order.getItems()) {
    validateItem(item);
  }
}

private Money calculateTotal(Order order) {
  Money subtotal = calculateSubtotal(order);
  Money discount = calculateDiscount(order, subtotal);
  Money tax = calculateTax(subtotal.minus(discount));
  return subtotal.minus(discount).plus(tax);
}

private void saveOrder(Order order) {
  order.setStatus(OrderStatus.CONFIRMED);
  database.save(order);
}

private void sendConfirmation(Order order) {
  Email email = Email.builder()
      .to(order.getCustomer().getEmail())
      .subject("Order Confirmation")
      .body("Your order total: " + order.getTotal())
      .build();
  emailService.send(email);
}

2.2 功能内聚

2.2.1 一个函数只做一件事

功能内聚是最高级别的内聚:函数内所有操作共同完成一个功能。

java
// 功能内聚:所有操作都服务于"验证用户登录"
public LoginResult validateLogin(String username, String password) {
  User user = findUserByUsername(username);
  if (user == null) {
    return LoginResult.userNotFound();
  }
  
  if (!passwordEncoder.matches(password, user.getPasswordHash())) {
    return LoginResult.invalidPassword();
  }
  
  if (user.isLocked()) {
    return LoginResult.accountLocked();
  }
  
  return LoginResult.success(user);
}

// 非功能内聚:做了多件不相关的事
public void processUser(String username, String password) {
  // 验证登录
  User user = findUserByUsername(username);
  
  // 记录日志(不相关)
  logger.info("User login attempt: " + username);
  
  // 更新统计(不相关)
  stats.incrementLoginCount();
  
  // 发送通知(不相关)
  if (user != null) {
    notificationService.sendLoginAlert(user);
  }
}

2.2.2 抽象层级

每个函数应该在一个抽象层级上工作。高层函数描述"做什么",低层函数描述"怎么做"。

java
// 抽象层级一致:都在"处理订单"这个层级
public OrderResult processOrder(Order order) {
  validateOrder(order);          // 同一层级
  Money total = calculateTotal(order);  // 同一层级
  saveOrder(order);              // 同一层级
  notifyCustomer(order);         // 同一层级
  return OrderResult.success(order.getId());
}

// 抽象层级混乱:混合了高层和低层操作
public OrderResult processOrder(Order order) {
  validateOrder(order);
  // 突然跳到低层:直接操作数据库
  Connection conn = DriverManager.getConnection(DB_URL);
  PreparedStatement stmt = conn.prepareStatement("INSERT INTO orders ...");
  stmt.setString(1, order.getId());
  stmt.executeUpdate();
  // 又跳回高层
  notifyCustomer(order);
  return OrderResult.success(order.getId());
}

2.3 参数设计

2.3.1 参数数量的原则

McConnell 建议:函数参数 越少越好,最好不超过 3 个。参数过多时,应该考虑:

  • 合并相关参数为对象
  • 拆分函数为多个小函数
  • 使用 Builder 模式
java
// 差:参数过多
public void createOrder(String customerName, String customerEmail,
                        String productName, int quantity, double price,
                        String shippingAddress, String city, String zipCode) {
  // ...
}

// 好:使用对象封装参数
public void createOrder(CustomerInfo customer, ProductInfo product,
                        ShippingInfo shipping) {
  // ...
}

// 更好:使用 Builder
public Order createOrder(CreateOrderRequest request) {
  // request 通过 Builder 构建
}

2.3.2 参数的顺序

参数顺序应该遵循以下原则:

  • 先必选后可选
  • 先输入后输出
  • 按逻辑顺序排列
java
// 好的参数顺序
public Money calculateTax(Money amount, TaxRate rate) {
  // amount 是输入,rate 是配置
  return amount.multiply(rate.getValue());
}

// 先必选后可选
public void sendEmail(String to, String subject, String body,
                      Optional<String> cc, Optional<String> bcc) {
  // ...
}

2.3.3 避免标志参数

标志参数(boolean 参数)通常意味着函数做了两件事,应该拆分。

java
// 差:标志参数
public void processOrder(Order order, boolean isExpress) {
  if (isExpress) {
    // 快速处理
  } else {
    // 标准处理
  }
}

// 好:拆分为两个函数
public void processStandardOrder(Order order) {
  // 标准处理
}

public void processExpressOrder(Order order) {
  // 快速处理
}

2.4 返回值设计

2.4.1 返回值的清晰性

函数的返回值应该清晰表达结果。避免使用"魔术值"。

java
// 差:返回魔术值
public int checkUserStatus(String username) {
  if (username == null) return -1;
  if (!exists(username)) return 0;
  if (isLocked(username)) return 1;
  return 2;
}

// 好:使用枚举
public UserStatus checkUserStatus(String username) {
  if (username == null) {
    throw new IllegalArgumentException("Username required");
  }
  if (!exists(username)) return UserStatus.NOT_FOUND;
  if (isLocked(username)) return UserStatus.LOCKED;
  return UserStatus.ACTIVE;
}

2.4.2 返回结果对象

当函数可能返回多种结果时,使用结果对象。

java
// 使用结果对象
public class OrderResult {
  private final boolean success;
  private final OrderId orderId;
  private final String errorMessage;
  
  private OrderResult(boolean success, OrderId orderId, String errorMessage) {
    this.success = success;
    this.orderId = orderId;
    this.errorMessage = errorMessage;
  }
  
  public static OrderResult success(OrderId orderId) {
    return new OrderResult(true, orderId, null);
  }
  
  public static OrderResult failure(String errorMessage) {
    return new OrderResult(false, null, errorMessage);
  }
  
  public boolean isSuccess() { return success; }
  public OrderId getOrderId() { return orderId; }
  public String getErrorMessage() { return errorMessage; }
}

public OrderResult createOrder(CreateOrderRequest request) {
  try {
    Order order = buildOrder(request);
    repository.save(order);
    return OrderResult.success(order.getId());
  } catch (Exception e) {
    return OrderResult.failure(e.getMessage());
  }
}

2.5 函数命名

2.5.1 命名规则

好的函数名应该:

  • 动词开头:表示动作
  • 描述意图:说明"做什么"而非"怎么做"
  • 避免模糊:不用 process、handle、manage 等模糊词
java
// 差:模糊的命名
public void process(Object obj) { ... }
public void handle(Event event) { ... }
public void doSomething() { ... }

// 好:清晰的命名
public OrderResult submitOrder(CreateOrderRequest request) { ... }
public void sendOrderConfirmationEmail(Order order) { ... }
public Money calculateOrderTotal(Order order) { ... }
public boolean isValidEmail(String email) { ... }

2.5.2 不同函数类型的命名

java
// 获取数据:get/find/query
public User getUser(UserId id) { ... }
public List<Order> findOrdersByCustomer(CustomerId customerId) { ... }

// 修改数据:create/update/delete/save
public Order createOrder(CreateOrderRequest request) { ... }
public void updateOrderStatus(OrderId id, OrderStatus status) { ... }

// 判断条件:is/has/can/should
public boolean isValidEmail(String email) { ... }
public boolean hasPermission(User user, Resource resource) { ... }
public boolean canBeCancelled(Order order) { ... }

// 转换数据:to/convert/parse/format
public UserDto toDto(User user) { ... }
public LocalDate parseDate(String dateStr) { ... }
public String formatCurrency(Money amount) { ... }

2.6 避免副作用

2.6.1 什么是副作用

副作用是指函数除了返回值之外,还修改了外部状态。副作用使代码难以理解、难以测试。

java
// 有副作用的函数
public class BadCalculator {
  private int lastResult; // 外部状态
  
  public int add(int a, int b) {
    lastResult = a + b; // 副作用:修改了外部状态
    System.out.println("Result: " + lastResult); // 副作用:I/O
    return lastResult;
  }
}

// 纯函数:没有副作用
public class GoodCalculator {
  public int add(int a, int b) {
    return a + b; // 只依赖输入,只影响返回值
  }
}

2.6.2 减少副作用的策略

java
// 策略1:使用不可变对象
public final class Money {
  private final double amount;
  private final String currency;
  
  public Money add(Money other) {
    // 返回新对象,不修改当前对象
    return new Money(this.amount + other.amount, this.currency);
  }
}

// 策略2:明确标注副作用
public class OrderService {
  // 方法名暗示了副作用
  public void saveOrder(Order order) {
    repository.save(order); // 明确知道会修改数据库
  }
  
  // 方法名暗示了无副作用
  public Money calculateTotal(Order order) {
    // 明确知道不会修改任何外部状态
    return order.getItems().stream()
        .mapToDouble(item -> item.getPrice() * item.getQuantity())
        .sum();
  }
}

三、实战案例

3.1 案例一:重构一个复杂函数

java
// 重构前:复杂的函数
public String generateReport(List<Order> orders, String format, 
                             boolean includeTax, boolean includeShipping) {
  String report = "";
  double totalAmount = 0;
  double totalTax = 0;
  double totalShipping = 0;
  
  for (Order order : orders) {
    double orderAmount = 0;
    for (OrderItem item : order.getItems()) {
      orderAmount += item.getPrice() * item.getQuantity();
    }
    totalAmount += orderAmount;
    
    if (includeTax) {
      double tax = orderAmount * 0.08;
      totalTax += tax;
    }
    
    if (includeShipping) {
      double shipping = orderAmount > 100 ? 0 : 10;
      totalShipping += shipping;
    }
    
    report += "Order " + order.getId() + ": $" + orderAmount + "\n";
  }
  
  double grandTotal = totalAmount + totalTax + totalShipping;
  report += "Subtotal: $" + totalAmount + "\n";
  if (includeTax) report += "Tax: $" + totalTax + "\n";
  if (includeShipping) report += "Shipping: $" + totalShipping + "\n";
  report += "Total: $" + grandTotal + "\n";
  
  if (format.equals("html")) {
    report = "<html><body><pre>" + report + "</pre></body></html>";
  }
  
  return report;
}

// 重构后:清晰的函数
public class ReportGenerator {
  
  public String generateReport(List<Order> orders, ReportConfig config) {
    ReportData data = calculateReportData(orders, config);
    String textReport = formatTextReport(data);
    
    if (config.isHtmlFormat()) {
      return wrapInHtml(textReport);
    }
    return textReport;
  }
  
  private ReportData calculateReportData(List<Order> orders, ReportConfig config) {
    ReportData data = new ReportData();
    
    for (Order order : orders) {
      Money orderAmount = calculateOrderAmount(order);
      data.addOrderLine(order.getId(), orderAmount);
      data.addToSubtotal(orderAmount);
      
      if (config.includeTax()) {
        Money tax = calculateTax(orderAmount);
        data.addToTax(tax);
      }
      
      if (config.includeShipping()) {
        Money shipping = calculateShipping(orderAmount);
        data.addToShipping(shipping);
      }
    }
    
    return data;
  }
  
  private Money calculateOrderAmount(Order order) {
    return order.getItems().stream()
        .map(item -> item.getPrice().multiply(item.getQuantity()))
        .reduce(Money.ZERO, Money::add);
  }
  
  private Money calculateTax(Money amount) {
    return amount.multiply(TaxRate.STANDARD);
  }
  
  private Money calculateShipping(Money orderAmount) {
    return orderAmount.greaterThan(Money.of(100)) 
        ? Money.ZERO 
        : Money.of(10);
  }
  
  private String formatTextReport(ReportData data) {
    StringBuilder sb = new StringBuilder();
    
    for (ReportLine line : data.getOrderLines()) {
      sb.append("Order ").append(line.getOrderId())
        .append(": $").append(line.getAmount()).append("\n");
    }
    
    sb.append("Subtotal: $").append(data.getSubtotal()).append("\n");
    if (data.getTax().isPositive()) {
      sb.append("Tax: $").append(data.getTax()).append("\n");
    }
    if (data.getShipping().isPositive()) {
      sb.append("Shipping: $").append(data.getShipping()).append("\n");
    }
    sb.append("Total: $").append(data.getGrandTotal()).append("\n");
    
    return sb.toString();
  }
  
  private String wrapInHtml(String textReport) {
    return "<html><body><pre>" + textReport + "</pre></body></html>";
  }
}

四、本章小结

第7章讲解了高质量函数的设计原则,核心要点包括:

函数要短小:理想长度 7-10 行,最长不超过 50 行。

只做一件事:功能内聚是最高目标。

抽象层级一致:每个函数在一个抽象层级上工作。

参数越少越好:不超过 3 个,过多时用对象封装。

返回值要清晰:避免魔术值,使用枚举或结果对象。

命名要表达意图:动词+名词,避免模糊词。

避免副作用:尽量编写纯函数。

McConnell 的核心观点是:写好函数是写好代码的基础。掌握函数设计的原则,就能写出清晰、可维护的代码。


五、思考与练习

思考题

你编写的函数平均有多长?有没有超过 50 行的?如何拆分?

你的函数中有多少个参数?参数过多时你是如何处理的?

你的函数有副作用吗?如何减少副作用?

你的函数命名是否清晰?有没有模糊的命名?

编程练习

找到一个超过 50 行的函数,将其拆分为多个小函数。

重构一个参数超过 5 个的函数,使用对象封装参数。

将一个有副作用的函数改造为纯函数。

7/12高质量的
0%