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 更清晰、更安全、更易维护 减少副作用 · 消除重复 · 提升可读性 适用场景 有副作用的查询方法 调用方需要明确意图 适用场景 多个函数逻辑相似 只有常量或类型不同 适用场景 boolean 参数控制分支 枚举参数区分行为

小结

今天聊的三个手法,其实都指向同一个目标:让 API 的意图更加明确。查询和修改分离,让调用方不用担心副作用;函数参数化,消除重复代码;移除标记参数,让每个方法只做一件事。

这些手法看起来简单,但真正用好需要练习。我建议你在下次重构时,先找出那些「看起来不对劲」的方法——比如一个方法里既有查询又有修改,或者参数里藏着 boolean 开关——然后试着用今天的手法去改造。你会发现,代码会变得越来越「听话」。

一句话总结:好的 API 设计,应该让调用方看一眼方法签名就知道「这个调用会改变什么」和「这个调用会返回什么」。


公众号:蓝海资料掘金营,微信 deep3321