重构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);
这里 low 和 high 明显是成对出现的,属于同一个「范围」概念。拆开传,调用方得自己拆包,函数签名也变长了。
重构后:
// 重构后
function isWithinRange(range, value) {
return value >= range.low && value <= range.high;
}
// 调用方直接传对象
isWithinRange(range, 15);
这样做的好处很明显:
- 参数变少,函数签名更简洁
- 调用方更省事,不用手动拆对象
- 未来扩展方便——如果 range 以后加个
unit字段,函数内部可以直接用,调用方不用改
但也要注意:如果函数只需要对象里的一个字段,那就别传整个对象。否则会造成不必要的依赖,测试时还得构造整个对象。
知识体系总览
这四种手法其实围绕着一个核心:让参数列表更「干净」。我画了张图帮你理清思路:
总结一下
这四种手法,其实就两个维度:
- 参数该不该存在?——用「以查询取代参数」和「以参数取代查询」来决策
- 参数该怎么组织?——用「移除标记参数」和「保持对象完整」来优化
我在实际项目中,通常先看参数列表的长度。超过3个参数,我就开始警觉了。然后逐个审视:
- 这个参数能不能在函数内部查到?能就干掉。
- 这个参数是不是标记?是就拆函数。
- 这几个参数是不是来自同一个对象?是就合并。
嗯,这套流程走下来,参数列表基本就清爽了。代码的可读性和可维护性,也会上一个台阶。