重构API(二):以查询取代参数、以参数取代查询、移除标记参数、保持对象完整

好,咱们接着聊API重构。上一章我们处理了函数返回值那边的坏味道,这一章我们把目光聚焦在参数列表上。

说实话,参数列表是代码里最容易「腐烂」的地方之一。我见过一个函数,参数从3个膨胀到12个,最后没人敢碰。为什么会这样?因为大家习惯往里加参数,却很少想着怎么优化它。

今天这四种手法,就是专门用来给参数列表「瘦身」和「提纯」的。咱们一个一个来看。

1. 以查询取代参数

这个手法说白了就是:如果函数能自己查到某个值,就别让调用者传进来

我举个例子。假设你有一个订单处理函数:

// 重构前
function calculateTotal(order, taxRate) {
    return order.items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

// 调用方每次都要传 taxRate
const total = calculateTotal(order, getCurrentTaxRate());

你看,taxRate 这个参数,其实函数内部完全可以自己获取。调用方每次都要传,既啰嗦又容易出错。

重构后:

// 重构后
function calculateTotal(order) {
    const taxRate = getCurrentTaxRate();  // 函数自己查
    return order.items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate);
}

// 调用方清爽多了
const total = calculateTotal(order);
我的经验:这种手法特别适合「配置类参数」——比如税率、折扣率、汇率这些。它们通常来自某个全局配置或上下文,没必要让调用方层层传递。

不过要注意一点:如果函数有多个数据源,或者查询操作有副作用,就别这么干。纯函数才是我们的目标。

2. 以参数取代查询

这个手法正好反过来。什么时候用?当函数内部依赖了一个「不稳定」的全局状态时。

我记得有一次接手一个支付系统,里面有个函数:

// 重构前
function processPayment(order) {
    const user = getCurrentUser();  // 从全局会话获取
    // ... 处理支付逻辑
}

这个 getCurrentUser() 依赖的是当前线程的会话上下文。在单元测试里,你得 mock 整个会话环境,特别痛苦。

重构后:

// 重构后
function processPayment(order, user) {
    // ... 处理支付逻辑,user 由调用方传入
}

// 测试时直接传一个 mock user 即可
processPayment(testOrder, { id: 1, name: 'Test' });
避坑指南:我曾经在一个项目中,把「查询当前时间」也改成了参数传入。结果发现调用方到处传 new Date(),反而更乱了。后来我意识到:只有「不稳定」或「难以模拟」的查询才值得替换。像当前时间这种,其实用 Date.now() 内部获取更合理。

说白了,这两个手法是一对「跷跷板」:查询稳定就用查询取代参数,查询不稳定就用参数取代查询。关键看你的函数想要什么样的「纯度」和「可测试性」。

3. 移除标记参数

标记参数,就是那种 true/false 或者枚举值,用来控制函数走不同分支的。

你想想看,一个函数里出现 if (flag) { ... } else { ... },这本身就是一种坏味道。它意味着这个函数在做两件不同的事。

// 重构前
function bookConcert(customer, isPremium) {
    if (isPremium) {
        // 高级会员:优先选座 + 赠送饮品
        premiumBooking(customer);
    } else {
        // 普通会员:标准流程
        regularBooking(customer);
    }
}

// 调用方
bookConcert(customer, true);   // 这 true 是什么意思?得看函数签名
bookConcert(customer, false);

重构后,拆成两个函数:

// 重构后
function premiumBookConcert(customer) {
    premiumBooking(customer);
}

function regularBookConcert(customer) {
    regularBooking(customer);
}

// 调用方一目了然
premiumBookConcert(customer);
regularBookConcert(customer);
核心原则:一个函数只做一件事。标记参数往往意味着「一件事没做完」。

当然,有时候标记参数确实能减少重复代码。比如日志库的 log(message, isError),拆成 logInfo(message)logError(message) 反而更清晰。我个人习惯是:如果标记参数只在一个地方使用,果断拆;如果到处都用,考虑用策略模式

4. 保持对象完整

这个手法讲的是:如果函数需要多个来自同一个对象的值,那就直接把整个对象传进去,而不是拆开传。

来看个反面例子:

// 重构前
function isWithinRange(low, high, value) {
    return value >= low && value <= high;
}

// 调用方
const range = { low: 10, high: 20 };
isWithinRange(range.low, range.high, 15);

这里 lowhigh 明显是成对出现的,属于同一个「范围」概念。拆开传,调用方得自己拆包,函数签名也变长了。

重构后:

// 重构后
function isWithinRange(range, value) {
    return value >= range.low && value <= range.high;
}

// 调用方直接传对象
isWithinRange(range, 15);

这样做的好处很明显:

  • 参数变少,函数签名更简洁
  • 调用方更省事,不用手动拆对象
  • 未来扩展方便——如果 range 以后加个 unit 字段,函数内部可以直接用,调用方不用改
我的经验:这个手法特别适合「值对象」或「数据传输对象」。比如地址、日期范围、坐标点这些。它们天然就是一组相关数据,应该作为一个整体传递。

但也要注意:如果函数只需要对象里的一个字段,那就别传整个对象。否则会造成不必要的依赖,测试时还得构造整个对象。

知识体系总览

这四种手法其实围绕着一个核心:让参数列表更「干净」。我画了张图帮你理清思路:

参数列表重构 以查询取代参数 以参数取代查询 移除标记参数 保持对象完整 核心原则:参数越少越好,依赖越明确越好 「查询稳定就用查询取代参数,查询不稳定就用参数取代查询」

总结一下

这四种手法,其实就两个维度:

  • 参数该不该存在?——用「以查询取代参数」和「以参数取代查询」来决策
  • 参数该怎么组织?——用「移除标记参数」和「保持对象完整」来优化

我在实际项目中,通常先看参数列表的长度。超过3个参数,我就开始警觉了。然后逐个审视:

  1. 这个参数能不能在函数内部查到?能就干掉。
  2. 这个参数是不是标记?是就拆函数。
  3. 这几个参数是不是来自同一个对象?是就合并。

嗯,这套流程走下来,参数列表基本就清爽了。代码的可读性和可维护性,也会上一个台阶。


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