第3章 函数
一、导读
1.1 本章学习目标
第3章详细讲解了函数设计的艺术。Uncle Bob 强调,函数的第一规则是短小,第二规则是只做一件事。本章的核心学习目标包括:
- 掌握函数要短小:ideally 20行以内。
- 理解只做一件事:单一职责原则。
- 学会抽象层级管理:每个函数在一个抽象层级上工作。
- 掌握参数设计:越少越好,避免标志参数。
- 理解函数命名:动词+名词,表达意图。
- 避免副作用:函数不应该做未声明的事。
1.2 为什么本章如此重要
函数是代码的基本构建块。Uncle Bob 指出:如果你能写好函数,你就能写好代码。函数的质量直接决定了代码的可读性、可测试性和可维护性。
本章的重要性在于它提供了函数设计的系统化方法。你会理解为什么函数应该短小、为什么应该只做一件事、如何管理抽象层级、如何设计参数。
1.3 核心问题引导
函数的理想长度是多少?为什么?
什么是"只做一件事"?如何判断?
抽象层级是什么意思?如何保持一致?
函数参数应该有多少个?过多时如何处理?
副作用为什么危险?如何避免?
二、核心概念详解
2.1 函数要短小
2.1.1 短小的标准
Uncle Bob 的建议:函数应该短小, ideally 20行以内,最长不超过50行。
短小的函数更容易理解、测试和复用。
// 差:过长的函数
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 什么是"一件事"
函数应该只做一件事,做好它。"一件事"意味着:函数内的所有操作都在同一抽象层级上,共同完成一个功能。
// 差:做了多件事
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 保持一致的抽象层级
每个函数应该在一个抽象层级上工作。高层函数描述"做什么",低层函数描述"怎么做"。
// 差:抽象层级混乱
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 自顶向下阅读
好的函数应该像一篇好文章,可以自顶向下阅读。
// 自顶向下阅读
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):尽量避免
- 三个及以上:应该避免
// 差:参数过多
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 参数)意味着函数做了两件事。
// 差:标志参数
public void render(boolean isSuite) {
if (isSuite) {
// 渲染套房
} else {
// 渲染普通房间
}
}
// 好:拆分为两个函数
public void renderSuite() {
// 渲染套房
}
public void renderStandardRoom() {
// 渲染普通房间
}2.5 副作用
2.5.1 副作用的危害
副作用是指函数除了返回值之外,还修改了外部状态。副作用使代码难以理解、难以测试。
// 有副作用
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 等
// 差
public void doStuff() { ... }
public void handle() { ... }
// 好
public void submitOrder(CreateOrderRequest request) { ... }
public void sendConfirmationEmail(Order order) { ... }
public Money calculateTotal(Order order) { ... }三、实战案例
3.1 案例一:重构一个复杂函数
// 重构前
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个的函数。
将一个有副作用的函数改造为纯函数。