第7章 高质量的子程序
一、导读
1.1 本章学习目标
第7章讲解了如何创建高质量的子程序(函数/方法)。McConnell 强调,函数是代码的基本构建块,其质量直接决定了整体代码质量。本章的核心学习目标包括:
- 掌握函数设计的核心原则:短小精悍,专注单一职责。
- 理解功能内聚的重要性:一个函数只做一件事,做好它。
- 学会合理的参数设计:数量适中,语义明确。
- 掌握返回值设计的技巧:清晰表达函数的结果。
- 理解函数命名的艺术:动词+名词,表达意图。
- 避免副作用:函数应该是纯函数。
1.2 为什么本章如此重要
函数是代码的最小可复用单元。McConnell 指出:如果你能写好函数,你就能写好代码。函数的质量直接决定了代码的可读性、可测试性和可维护性。
本章的重要性在于它提供了函数设计的系统化方法。你会理解为什么有些函数清晰易懂,有些混乱难懂,以及如何写出高质量的函数。
1.3 核心问题引导
函数的理想长度是多少?如何判断一个函数是否太长?
什么是"单一职责"?一个函数如何做到"只做一件事"?
函数参数应该有多少个?参数过多时如何处理?
为什么副作用是危险的?如何编写纯函数?
好的函数命名应该遵循什么规则?
二、核心概念详解
2.1 函数的长度
2.1.1 短小精悍
McConnell 引用研究数据表明:函数的理想长度在 7-10 行之间,最长不超过 50 行。过长的函数难以理解、难以测试、难以维护。
// 差:过长的函数(做了太多事)
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 一个函数只做一件事
功能内聚是最高级别的内聚:函数内所有操作共同完成一个功能。
// 功能内聚:所有操作都服务于"验证用户登录"
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 抽象层级
每个函数应该在一个抽象层级上工作。高层函数描述"做什么",低层函数描述"怎么做"。
// 抽象层级一致:都在"处理订单"这个层级
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 模式
// 差:参数过多
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 参数的顺序
参数顺序应该遵循以下原则:
- 先必选后可选
- 先输入后输出
- 按逻辑顺序排列
// 好的参数顺序
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 参数)通常意味着函数做了两件事,应该拆分。
// 差:标志参数
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 返回值的清晰性
函数的返回值应该清晰表达结果。避免使用"魔术值"。
// 差:返回魔术值
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 返回结果对象
当函数可能返回多种结果时,使用结果对象。
// 使用结果对象
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 等模糊词
// 差:模糊的命名
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 不同函数类型的命名
// 获取数据: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 什么是副作用
副作用是指函数除了返回值之外,还修改了外部状态。副作用使代码难以理解、难以测试。
// 有副作用的函数
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 减少副作用的策略
// 策略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 案例一:重构一个复杂函数
// 重构前:复杂的函数
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 个的函数,使用对象封装参数。
将一个有副作用的函数改造为纯函数。