16、重构API(上):将查询函数和修改函数分离、函数参数化、移除标记参数
API 重构这块,说实话,是日常开发里最容易「埋雷」的地方。你想想看,一个接口设计得不好,调用方就得写一堆补丁代码,久而久之,整个系统就像打了无数补丁的旧衣服——看着还能穿,但谁都不敢使劲扯。
今天咱们聊三个非常实用的 API 重构手法。它们不复杂,但用好了,代码质量能上一个台阶。
将查询函数和修改函数分离
这个原则其实很简单:一个函数要么做查询,要么做修改,别混着来。我见过太多代码,一个方法里既改了状态又返回了数据,调用方根本搞不清调用后会不会有副作用。
核心原则:任何有副作用的函数,都不应该同时返回数据。反之,纯查询函数不应该修改任何状态。
举个例子,假设我们有这样一个方法:
// 重构前:查询和修改混在一起
public class SecurityService {
private List<String> loggedUsers = new ArrayList<>();
public String checkUserAndLog(String userName) {
// 副作用:记录了日志
loggedUsers.add(userName);
// 查询:返回了用户状态
return findUserStatus(userName);
}
}
这个方法的调用者,如果不看实现细节,根本不知道它偷偷改了 loggedUsers 列表。我在项目中遇到过类似的情况——一个同事调用了「看起来只是查询」的方法,结果线上出现了奇怪的状态不一致。排查了半天,才发现那个方法里偷偷写了个缓存。
重构后,应该拆成两个方法:
// 重构后:查询和修改分离
public class SecurityService {
private List<String> loggedUsers = new ArrayList<>();
// 纯修改函数
public void logUserAccess(String userName) {
loggedUsers.add(userName);
}
// 纯查询函数
public String getUserStatus(String userName) {
return findUserStatus(userName);
}
}
调用方可以按需组合:
// 调用方明确知道自己在做什么
service.logUserAccess(userName);
String status = service.getUserStatus(userName);
我的习惯:给方法命名时,如果方法名以「get」开头,我绝不让它修改任何状态。反过来,以「set」「update」「log」开头的方法,我绝不让它返回有意义的数据(最多返回 boolean 表示成功与否)。
函数参数化
函数参数化,说白了就是把重复的逻辑抽象成一个通用函数,用参数来控制行为差异。你想想看,如果两段代码只有某个常量不同,其他逻辑一模一样,那为什么不合并呢?
来看一个典型的坏味道:
// 重构前:两个几乎一样的函数
public double baseChargeForResidential() {
double usage = getUsage("residential");
double rate = 0.12;
return usage * rate;
}
public double baseChargeForCommercial() {
double usage = getUsage("commercial");
double rate = 0.18;
return usage * rate;
}
这两个函数除了客户类型和费率不同,其他逻辑完全一样。重构后:
// 重构后:参数化
public double baseCharge(String customerType) {
double usage = getUsage(customerType);
double rate = getRateByType(customerType);
return usage * rate;
}
private double getRateByType(String customerType) {
Map<String, Double> rates = Map.of(
"residential", 0.12,
"commercial", 0.18,
"industrial", 0.22
);
return rates.getOrDefault(customerType, 0.15);
}
调用方只需要传一个参数:
double charge = baseCharge("commercial");
我曾经在一个遗留系统里看到过 7 个几乎一样的函数,区别只是硬编码的阈值不同。合并成一个参数化函数后,代码量减少了 60%,而且新增一种类型只需要加一行配置,不用再复制粘贴整个函数了。
注意:参数化不是万能的。如果两个函数的逻辑差异超过 30%,强行参数化反而会让代码变得难以理解。这时候,保留两个独立函数可能更清晰。
移除标记参数
标记参数,就是那种 boolean 或者枚举类型的参数,用来控制函数内部走哪条分支。比如:
// 重构前:使用标记参数
public void bookConference(String name, boolean isVIP) {
if (isVIP) {
// VIP 预订流程:优先安排、赠送礼品
assignPriorityRoom(name);
addGiftPackage(name);
} else {
// 普通预订流程
assignStandardRoom(name);
}
sendConfirmation(name);
}
这个 isVIP 参数就是个典型的标记参数。调用方看到 bookConference("张三", true),得去查文档才知道 true 是什么意思。而且,这种设计很容易导致函数内部出现大量 if-else 分支,越来越臃肿。
重构方案:拆成两个明确的方法。
// 重构后:移除标记参数
public void bookVIPConference(String name) {
assignPriorityRoom(name);
addGiftPackage(name);
sendConfirmation(name);
}
public void bookStandardConference(String name) {
assignStandardRoom(name);
sendConfirmation(name);
}
调用方现在一目了然:
bookVIPConference("张三");
bookStandardConference("李四");
嗯,这里要注意:如果标记参数控制的是多个独立的行为,比如一个 boolean 控制是否发送邮件,另一个 boolean 控制是否记录日志,那拆成多个方法反而会爆炸。这时候,可以考虑用策略模式或者构建器模式来处理。
避坑指南:我曾经接手过一个项目,一个方法有 4 个 boolean 参数,组合起来有 16 种行为。调用方传参时写了一大串 true/false,根本没人能看懂。后来我花了三天,拆成了 8 个有明确语义的方法,配合策略模式,代码可读性提升了好几个档次。
知识体系总览
下面这张图总结了今天讲的三个重构手法,以及它们之间的关系:
小结
今天聊的三个手法,其实都指向同一个目标:让 API 的意图更加明确。查询和修改分离,让调用方不用担心副作用;函数参数化,消除重复代码;移除标记参数,让每个方法只做一件事。
这些手法看起来简单,但真正用好需要练习。我建议你在下次重构时,先找出那些「看起来不对劲」的方法——比如一个方法里既有查询又有修改,或者参数里藏着 boolean 开关——然后试着用今天的手法去改造。你会发现,代码会变得越来越「听话」。
一句话总结:好的 API 设计,应该让调用方看一眼方法签名就知道「这个调用会改变什么」和「这个调用会返回什么」。
公众号:蓝海资料掘金营,微信 deep3321