3代码整洁之道

函数

函数设计

短小单一职责抽象层级

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

第3章 函数

一、导读

1.1 本章学习目标

第3章详细讲解了函数设计的艺术。Uncle Bob 强调,函数的第一规则是短小,第二规则是只做一件事。本章的核心学习目标包括:

  • 掌握函数要短小:ideally 20行以内。
  • 理解只做一件事:单一职责原则。
  • 学会抽象层级管理:每个函数在一个抽象层级上工作。
  • 掌握参数设计:越少越好,避免标志参数。
  • 理解函数命名:动词+名词,表达意图。
  • 避免副作用:函数不应该做未声明的事。

1.2 为什么本章如此重要

函数是代码的基本构建块。Uncle Bob 指出:如果你能写好函数,你就能写好代码。函数的质量直接决定了代码的可读性、可测试性和可维护性。

本章的重要性在于它提供了函数设计的系统化方法。你会理解为什么函数应该短小、为什么应该只做一件事、如何管理抽象层级、如何设计参数。

1.3 核心问题引导

函数的理想长度是多少?为什么?

什么是"只做一件事"?如何判断?

抽象层级是什么意思?如何保持一致?

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

副作用为什么危险?如何避免?


二、核心概念详解

2.1 函数要短小

2.1.1 短小的标准

Uncle Bob 的建议:函数应该短小, ideally 20行以内,最长不超过50行。

短小的函数更容易理解、测试和复用。

java
// 差:过长的函数
public void processOrder(Order order) {
  // 验证订单(20行)
  // 计算价格(15行)
  // 保存订单(10行)
  // 发送通知(10行)
  // 总计55行
}

// 好:短小的函数
public void processOrder(Order order) {
  validateOrder(order);
  Money total = calculateTotal(order);
  saveOrder(order, total);
  sendConfirmation(order);
}

2.2 只做一件事

2.2.1 什么是"一件事"

函数应该只做一件事,做好它。"一件事"意味着:函数内的所有操作都在同一抽象层级上,共同完成一个功能。

java
// 差:做了多件事
public void processOrder(Order order) {
  // 验证(一件事)
  if (order == null) throw new IllegalArgumentException();
  if (order.getItems().isEmpty()) throw new IllegalArgumentException();
  
  // 计算价格(另一件事)
  double total = 0;
  for (OrderItem item : order.getItems()) {
    total += item.getPrice() * item.getQuantity();
  }
  
  // 保存(又一件事)
  database.save(order);
  
  // 发邮件(还一件事)
  Email email = new Email();
  email.setTo(order.getCustomer().getEmail());
  email.send();
}

// 好:每个函数只做一件事
public void processOrder(Order order) {
  validateOrder(order);
  Money total = calculateTotal(order);
  order.setTotal(total);
  saveOrder(order);
  sendConfirmation(order);
}

2.2.2 如何判断是否"只做一件事"

一个判断标准:如果函数可以被拆分为多个独立的段落,每段可以提取为一个函数,那么原函数做了多件事。

2.3 抽象层级

2.3.1 保持一致的抽象层级

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

java
// 差:抽象层级混乱
public void processOrder(Order order) {
  validateOrder(order);           // 高层
  
  // 突然跳到低层
  Connection conn = DriverManager.getConnection(DB_URL);
  PreparedStatement stmt = conn.prepareStatement("INSERT ...");
  stmt.setString(1, order.getId());
  stmt.executeUpdate();
  
  sendConfirmation(order);        // 又回到高层
}

// 好:抽象层级一致
public void processOrder(Order order) {
  validateOrder(order);
  Money total = calculateTotal(order);
  saveOrder(order);
  sendConfirmation(order);
}
// 每个调用的函数都在同一抽象层级

2.3.2 自顶向下阅读

好的函数应该像一篇好文章,可以自顶向下阅读。

java
// 自顶向下阅读
public void processOrder(Order order) {
  validateOrder(order);           // 第一层:验证
  Money total = calculateTotal(order);  // 第一层:计算
  saveOrder(order);               // 第一层:保存
  sendConfirmation(order);        // 第一层:通知
}

// 每个函数可以进一步展开
private void validateOrder(Order order) {
  // 第二层:具体的验证逻辑
}

private Money calculateTotal(Order order) {
  // 第二层:具体的计算逻辑
}

2.4 参数设计

2.4.1 参数越少越好

Uncle Bob 的建议:

  • 零参数(niladic):最理想
  • 一个参数(monadic):可以接受
  • 两个参数(dyadic):尽量避免
  • 三个及以上:应该避免
java
// 差:参数过多
public void createOrder(String customerName, String email,
                        String product, int quantity, double price,
                        String address) {
  // ...
}

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

// 更好:使用请求对象
public OrderResult createOrder(CreateOrderRequest request) {
  // ...
}

2.4.2 避免标志参数

标志参数(boolean 参数)意味着函数做了两件事。

java
// 差:标志参数
public void render(boolean isSuite) {
  if (isSuite) {
    // 渲染套房
  } else {
    // 渲染普通房间
  }
}

// 好:拆分为两个函数
public void renderSuite() {
  // 渲染套房
}

public void renderStandardRoom() {
  // 渲染普通房间
}

2.5 副作用

2.5.1 副作用的危害

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

java
// 有副作用
public class SessionManager {
  private User currentUser;
  
  public boolean login(String username, String password) {
    User user = findUser(username);
    if (user != null && verifyPassword(password, user)) {
      currentUser = user; // 副作用:修改了外部状态
      return true;
    }
    return false;
  }
}

// 减少副作用
public class AuthenticationService {
  
  public LoginResult login(String username, String password) {
    User user = findUser(username);
    if (user == null) {
      return LoginResult.userNotFound();
    }
    if (!verifyPassword(password, user)) {
      return LoginResult.invalidPassword();
    }
    String token = generateToken(user);
    return LoginResult.success(token);
    // 不修改任何外部状态
  }
}

2.6 函数命名

2.6.1 命名规则

好的函数名应该:

  • 动词开头:表示动作
  • 描述意图:说明"做什么"
  • 避免模糊:不用 process、handle 等
java
// 差
public void doStuff() { ... }
public void handle() { ... }

// 好
public void submitOrder(CreateOrderRequest request) { ... }
public void sendConfirmationEmail(Order order) { ... }
public Money calculateTotal(Order order) { ... }

三、实战案例

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

java
// 重构前
public String generateReport(List<Order> orders, String format) {
  String report = "";
  double total = 0;
  for (Order order : orders) {
    double orderTotal = 0;
    for (OrderItem item : order.getItems()) {
      orderTotal += item.getPrice() * item.getQuantity();
    }
    total += orderTotal;
    report += "Order " + order.getId() + ": $" + orderTotal + "\n";
  }
  report += "Total: $" + total + "\n";
  if (format.equals("html")) {
    report = "<html><body><pre>" + report + "</pre></body></html>";
  }
  return report;
}

// 重构后
public class ReportGenerator {
  
  public String generate(List<Order> orders, ReportFormat format) {
    ReportData data = buildReportData(orders);
    String text = formatTextReport(data);
    
    if (format == ReportFormat.HTML) {
      return wrapInHtml(text);
    }
    return text;
  }
  
  private ReportData buildReportData(List<Order> orders) {
    ReportData data = new ReportData();
    for (Order order : orders) {
      Money orderTotal = calculateOrderTotal(order);
      data.addOrderLine(order.getId(), orderTotal);
      data.addToTotal(orderTotal);
    }
    return data;
  }
  
  private Money calculateOrderTotal(Order order) {
    return order.getItems().stream()
        .map(item -> item.getPrice().multiply(item.getQuantity()))
        .reduce(Money.ZERO, Money::add);
  }
  
  private String formatTextReport(ReportData data) {
    StringBuilder sb = new StringBuilder();
    for (ReportLine line : data.getLines()) {
      sb.append("Order ").append(line.getId())
        .append(": $").append(line.getAmount()).append("\n");
    }
    sb.append("Total: $").append(data.getTotal()).append("\n");
    return sb.toString();
  }
  
  private String wrapInHtml(String text) {
    return "<html><body><pre>" + text + "</pre></body></html>";
  }
}

四、本章小结

第3章讲解了函数设计的艺术,核心要点包括:

函数要短小:ideally 20行以内,最长不超过50行。

只做一件事:函数内所有操作在同一抽象层级上。

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

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

避免副作用:函数不应该做未声明的事。

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

Uncle Bob 的核心观点是:写好函数是写好代码的基础。


五、思考与练习

思考题

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

你的函数是否"只做一件事"?如何判断?

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

编程练习

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

重构一个参数超过5个的函数。

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

3/10函数
0%