12、API重构与数据组织:将查询函数和修改函数分离、函数参数化、移除标记参数、以查询取代参数、以命令取代函数
好,咱们今天聊点实在的。API重构这件事,说白了就是让代码的接口更干净、更不容易出错。我见过太多项目,API设计得跟迷宫似的,调用者战战兢兢,维护者叫苦连天。今天这五个手法,是我个人在实战中反复用到的,每一个都帮我解决过真实的问题。
12.1 将查询函数和修改函数分离
这个原则其实很简单:一个函数要么做查询,要么做修改,别混着来。为什么?你想想看,调用者调用一个函数,如果它既改了状态又返回了数据,那调用者很容易误用——比如只用了返回值,却忽略了副作用。
核心原则:查询函数不应产生副作用,修改函数不应返回数据(或只返回简单状态)。
我在项目中遇到过这样一个案例:有个函数叫 processOrder(orderId),它既更新了订单状态,又返回了订单详情。结果有个同事在只读场景下调用了它,导致订单状态被意外修改,线上出了个大问题。从那以后,我对这种「混合函数」就特别敏感。
来看个例子:
// 重构前:查询和修改混在一起
public Customer findCustomer(String id) {
Customer customer = customerRepository.findById(id);
if (customer == null) {
log.warn("Customer not found: " + id);
metrics.incrementMissCount(); // 副作用!
}
return customer;
}
// 重构后:分离查询和修改
public Customer findCustomer(String id) {
return customerRepository.findById(id);
}
public void recordMiss(String id) {
log.warn("Customer not found: " + id);
metrics.incrementMissCount();
}
我的习惯:写函数前先问自己——这个函数是「问」还是「做」?如果是「问」,就只返回值;如果是「做」,就别返回值(或只返回成功/失败状态)。
12.2 函数参数化
函数参数化,说白了就是把重复的函数合并成一个,用参数来控制行为差异。我见过很多代码库里,有三四个函数长得几乎一模一样,唯一的区别就是某个常量不同。这种重复,就是技术债务的温床。
为什么会这样?很多时候是因为一开始只想到了一种场景,后来需求变了,就复制粘贴改了个常量。嗯,这里要注意:复制粘贴一时爽,维护起来火葬场。
// 重构前:两个几乎一样的函数
public void chargeRegular(double amount) {
double fee = amount * 0.02;
// ... 处理逻辑
}
public void chargePremium(double amount) {
double fee = amount * 0.01;
// ... 处理逻辑
}
// 重构后:参数化
public void charge(double amount, CustomerType type) {
double fee = amount * (type == CustomerType.PREMIUM ? 0.01 : 0.02);
// ... 处理逻辑
}
| 场景 | 参数化前 | 参数化后 |
|---|---|---|
| 不同费率 | chargeRegular, chargePremium | charge(amount, type) |
| 不同排序 | sortByName, sortByDate | sort(comparator) |
| 不同输出格式 | toJson, toXml | export(format) |
避坑指南:我曾经把一个函数参数化搞过头了,传了七八个参数,调用方根本搞不清该怎么传。参数化不是越多越好,当参数超过3个时,就要考虑是不是该拆函数了。
12.3 移除标记参数
标记参数,就是那种布尔值或者枚举值,用来控制函数走哪条分支。比如 processOrder(order, true),这个 true 是什么意思?调用者得去翻文档,或者看函数实现才知道。
我个人觉得,标记参数是代码可读性的头号杀手。你想想看,一个函数内部根据一个布尔值走两条完全不同的逻辑,那还不如拆成两个函数,名字就能说明一切。
// 重构前:标记参数让人困惑
public void bookConferenceRoom(String roomId, boolean isVIP) {
if (isVIP) {
// VIP 预订逻辑:优先分配、免费茶水
} else {
// 普通预订逻辑:先到先得、收费服务
}
}
// 调用方:bookConferenceRoom("A101", true); // true 是什么意思?
// 重构后:拆成两个函数
public void bookConferenceRoomVIP(String roomId) {
// VIP 预订逻辑
}
public void bookConferenceRoomStandard(String roomId) {
// 普通预订逻辑
}
// 调用方:bookConferenceRoomVIP("A101"); // 一目了然
什么时候可以保留标记参数?当标记只影响一个很小的行为差异时,比如是否打印日志。但如果标记导致函数走完全不同的逻辑分支,一定要拆。
12.4 以查询取代参数
这个手法说的是:如果一个参数可以通过其他方式获取到,那就别传了,直接在函数内部查询。我见过太多函数,调用方需要先查一遍数据,然后把查到的结果作为参数传进去,函数内部又查一遍——这不是脱裤子放屁吗?
// 重构前:调用方需要先查询
public void sendInvoice(Order order, Customer customer) {
// 使用 order 和 customer 生成发票
}
// 调用方:
Order order = orderRepository.findById(orderId);
Customer customer = customerRepository.findById(order.getCustomerId());
sendInvoice(order, customer);
// 重构后:函数内部查询
public void sendInvoice(Order order) {
Customer customer = customerRepository.findById(order.getCustomerId());
// 使用 order 和 customer 生成发票
}
// 调用方:
Order order = orderRepository.findById(orderId);
sendInvoice(order); // 简洁多了
我的经验:这个手法特别适合那种「参数和调用对象有明确关联」的场景。比如订单和客户、文章和作者。但要注意,如果查询成本很高,或者函数需要保持纯函数特性,那就别这么干。
12.5 以命令取代函数
这个手法有点反直觉——我们平时都在说「函数要小」,怎么这里反而要把函数变成命令对象?其实,当一个函数变得太复杂,参数太多,或者需要维护状态时,把它封装成一个命令对象反而更清晰。
我曾经接手过一个支付处理函数,有十几个参数,内部逻辑长达200行。每次修改都提心吊胆。后来我把它重构成了一个命令对象,参数变成了构造函数的字段,逻辑拆成了几个小方法,可读性和可维护性都大幅提升。
// 重构前:一个臃肿的函数
public PaymentResult processPayment(
String orderId,
String userId,
BigDecimal amount,
String currency,
String paymentMethod,
String cardNumber,
String expiryDate,
String cvv,
String billingAddress,
boolean saveCard,
boolean sendReceipt
) {
// 200 行逻辑...
}
// 重构后:命令对象
public class ProcessPaymentCommand {
private String orderId;
private String userId;
private BigDecimal amount;
private String currency;
private String paymentMethod;
// ... 其他字段
public ProcessPaymentCommand(String orderId, String userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
}
// 链式调用设置可选参数
public ProcessPaymentCommand withCurrency(String currency) {
this.currency = currency;
return this;
}
public ProcessPaymentCommand withPaymentMethod(String method) {
this.paymentMethod = method;
return this;
}
public PaymentResult execute() {
// 拆分成多个小方法
validate();
calculateFees();
process();
notify();
return buildResult();
}
private void validate() { /* ... */ }
private void calculateFees() { /* ... */ }
private void process() { /* ... */ }
private void notify() { /* ... */ }
private PaymentResult buildResult() { /* ... */ }
}
// 调用方:
new ProcessPaymentCommand("ORD-001", "USR-001", new BigDecimal("99.99"))
.withCurrency("CNY")
.withPaymentMethod("CREDIT_CARD")
.execute();
注意:命令模式不是银弹。如果函数只有几行,参数也不多,强行搞成命令对象就是过度设计。我一般只在函数超过50行,或者参数超过5个时,才考虑用命令取代函数。
知识体系总览
下面这张图,是我对这五个手法的理解。它们不是孤立的,而是相互关联的。你可以把它们看作一套工具箱,遇到不同的代码坏味道,就拿出对应的工具来重构。
这五个手法,说白了就是围绕一个核心:让 API 的意图更清晰。调用方不需要猜,不需要翻文档,看一眼函数签名就知道该怎么做。我这些年重构过的代码,凡是遵循这些原则的,后续维护成本都大幅降低。
总结一下:
- 查询修改分离——别让调用者承担意外的副作用
- 函数参数化——用参数消除重复,但别过度
- 移除标记参数——布尔值参数是代码坏味道
- 以查询取代参数——能查到的就别让调用者传
- 以命令取代函数——复杂函数封装成对象,清晰可控
嗯,今天就聊到这儿。这些手法看着简单,但真正用好需要大量实践。下次你写代码时,不妨多问自己一句:这个 API 设计得够干净吗?
公众号:蓝海资料掘金营,微信 deep3321