重构API(下):保持对象完整、以查询取代参数、以参数取代查询、移除设值函数
好,咱们接着聊API重构。上一章我们讲了拆分、合并参数,还有引入参数对象。这一章要聊的几个手法,说白了都是在问一个问题:这个参数,真的有必要传吗?
我见过太多代码,方法签名长得像购物清单。调用方得先凑齐一堆参数,才能调用。这就像你去食堂打饭,阿姨说“你先给我看饭卡、再给我看学生证、还要出示健康码”——烦不烦?
嗯,今天我们就来解决这个问题。
保持对象完整
先看一个最常见的场景。你有一个对象,里面有好几个字段。然后你调用一个方法,只传了其中两个字段进去。
// 重构前
const name = user.name;
const age = user.age;
printUserInfo(name, age);
function printUserInfo(name, age) {
console.log(`${name} is ${age} years old`);
}
你想想看,这样写有什么问题?
第一,调用方得先拆开对象。第二,如果以后要加一个字段,比如加个邮箱,你得改两个地方:调用方要传新参数,方法内部也要改。第三,方法签名越来越长。
我个人习惯是:如果方法内部要用到某个对象的多个字段,那就直接把整个对象传进去。
// 重构后
printUserInfo(user);
function printUserInfo(user) {
console.log(`${user.name} is ${user.age} years old`);
}
我在项目中遇到过这样一个案例:有个订单处理的方法,传了订单ID、用户ID、商品ID、数量、单价……一共7个参数。后来需求变更,要加一个折扣字段。改完调用方改方法,改完方法改测试,折腾了一下午。如果当初直接传订单对象,加个字段就完事了。
以查询取代参数
这个手法很有意思。它的核心思想是:如果一个参数的值可以通过其他方式获取到,那就别传了,直接在方法内部查。
举个例子:
// 重构前
function getPrice(quantity, itemId, discountRate) {
const basePrice = quantity * getItemPrice(itemId);
return basePrice * (1 - discountRate);
}
// 调用方
const price = getPrice(10, 'item_001', 0.1);
这里有个问题:discountRate 这个参数,其实可以从商品信息里查出来。那为什么要让调用方传呢?
// 重构后
function getPrice(quantity, itemId) {
const basePrice = quantity * getItemPrice(itemId);
const discountRate = getDiscountRate(itemId);
return basePrice * (1 - discountRate);
}
// 调用方
const price = getPrice(10, 'item_001');
你看,调用方少传了一个参数。而且如果以后折扣规则变了,只需要改 getDiscountRate 这一个地方就行。
我曾经在一个项目中看到,有个方法传了6个参数,其中3个都是可以从数据库查出来的。调用方每次调用前都要先查一遍数据库,然后把查到的值传进去。这不是脱裤子放屁吗?
以参数取代查询
这个手法和上面那个正好相反。你可能会问:那到底什么时候该用哪个?
嗯,这里有个判断标准:如果方法内部有副作用,或者查询操作很耗时,那就用参数取代查询。
举个例子:
// 重构前
function calculateTotal(order) {
const taxRate = getTaxRate(order.city); // 这个查询可能很慢
return order.amount * (1 + taxRate);
}
如果 getTaxRate 是一个远程调用,每次都要等几百毫秒。那这个方法就变成了一个“慢方法”。而且测试的时候也不好测,因为 getTaxRate 依赖外部服务。
// 重构后
function calculateTotal(order, taxRate) {
return order.amount * (1 + taxRate);
}
// 调用方
const taxRate = getTaxRate(order.city);
const total = calculateTotal(order, taxRate);
这样一改,calculateTotal 变成了纯函数。测试起来特别方便,传什么参数就返回什么结果。而且调用方可以自己决定什么时候去查税率,甚至可以缓存。
- 如果查询操作没有副作用且很快 → 用查询取代参数
- 如果查询操作有副作用或很慢 → 用参数取代查询
移除设值函数
这个手法,说白了就是:别让外部随便改你的内部状态。
我见过很多代码,一个类里全是 getter 和 setter。每个字段都有 setter,外部想怎么改就怎么改。这就像你家大门敞开着,谁都能进来搬东西。
// 重构前
class User {
constructor(name, age) {
this._name = name;
this._age = age;
}
getName() { return this._name; }
setName(name) { this._name = name; }
getAge() { return this._age; }
setAge(age) { this._age = age; }
}
你想想看,setName 和 setAge 真的需要暴露吗?如果用户的姓名和年龄在创建后就不应该改变,那为什么要提供 setter?
// 重构后
class User {
constructor(name, age) {
this._name = name;
this._age = age;
}
getName() { return this._name; }
getAge() { return this._age; }
}
移除设值函数的好处很明显:
- 不可变性:对象一旦创建,状态就不会变。这能避免很多奇怪的 bug。
- 意图清晰:看到没有 setter,别人就知道这个字段是只读的。
- 减少耦合:外部不能随意修改内部状态,所有修改都必须通过方法。
知识体系总览
这一章讲的四个手法,其实都在解决同一个问题:参数太多了怎么办? 我画了一张图,帮你理清思路:
总结一下
这一章我们聊了四个手法:
| 手法 | 适用场景 | 核心思想 |
|---|---|---|
| 保持对象完整 | 方法内部使用对象的多个字段 | 传整个对象,别拆开传 |
| 以查询取代参数 | 参数值可以在方法内部获取 | 去掉参数,内部查询 |
| 以参数取代查询 | 查询有副作用或很慢 | 改为参数,让调用方控制 |
| 移除设值函数 | 字段创建后不应修改 | 去掉 setter,保持不可变 |
说白了,这些手法都是在帮你做减法。参数少了,代码就简单了。代码简单了,bug 就少了。bug 少了,你就能早点下班了。
嗯,今天就到这里。下一章我们聊重构中的“处理继承关系”,那又是一个大坑。