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 重构核心 查询与修改分离 一个函数只做一件事 函数参数化 用参数消除重复 移除标记参数 布尔值?拆成两个函数 以查询取代参数 能查到的就别传 以命令取代函数 复杂函数封装成对象 核心思想 让 API 的意图更清晰 · 减少调用方的认知负担 · 降低误用的概率

这五个手法,说白了就是围绕一个核心:让 API 的意图更清晰。调用方不需要猜,不需要翻文档,看一眼函数签名就知道该怎么做。我这些年重构过的代码,凡是遵循这些原则的,后续维护成本都大幅降低。

总结一下:

  • 查询修改分离——别让调用者承担意外的副作用
  • 函数参数化——用参数消除重复,但别过度
  • 移除标记参数——布尔值参数是代码坏味道
  • 以查询取代参数——能查到的就别让调用者传
  • 以命令取代函数——复杂函数封装成对象,清晰可控

嗯,今天就聊到这儿。这些手法看着简单,但真正用好需要大量实践。下次你写代码时,不妨多问自己一句:这个 API 设计得够干净吗?


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