17、重构API(下):保持对象完整、以查询取代参数、以参数取代查询、移除设值函数
好,咱们接着聊API重构。上一节我们讲了拆分查询、修改函数声明这些基础操作。这一节要聊的几个手法,说白了都是在问一个问题:你的函数到底需要什么?
我见过太多代码,函数签名长得像购物清单——恨不得把所有能用上的东西都列进去。结果呢?改一个地方,牵出一串连锁反应。今天讲的这四个手法,就是帮你把API接口收拾得干净利落。
保持对象完整
先看个例子。假设你有一个Room对象,里面存着房间的温度、湿度、光照等数据。现在要判断房间是否适合开会:
// 重构前:拆开传参
function isMeetingRoomAvailable(temperature, humidity, lightLevel, noiseLevel) {
// 判断逻辑...
}
// 调用方
const room = getRoomById(101);
const available = isMeetingRoomAvailable(
room.temperature,
room.humidity,
room.lightLevel,
room.noiseLevel
);
你发现问题了吗?调用方把room对象拆得七零八落,一个个传进去。如果以后Room新增了一个airQuality字段,所有调用isMeetingRoomAvailable的地方都得改。
重构后:
// 重构后:直接传对象
function isMeetingRoomAvailable(room) {
const { temperature, humidity, lightLevel, noiseLevel } = room;
// 判断逻辑...
}
// 调用方
const room = getRoomById(101);
const available = isMeetingRoomAvailable(room);
我个人习惯是:只要函数用到了同一个对象的三个或以上字段,就考虑传整个对象。当然,也不是无脑传。如果函数只需要对象里的一个字段,传整个对象反而增加了耦合。
以查询取代参数
这个手法很有意思。有时候你会发现,某个参数其实可以从函数内部获取到,根本不需要调用方传进来。
// 重构前:调用方需要传一个"当前用户"
function applyDiscount(order, currentUser) {
if (currentUser.isVIP) {
order.total *= 0.9;
}
}
// 调用方
applyDiscount(order, getCurrentUser());
你看,currentUser这个参数,每次调用都得传。而且调用方还得知道要传getCurrentUser()。这其实是个不必要的负担。
重构后:
// 重构后:函数内部自己查
function applyDiscount(order) {
const currentUser = getCurrentUser();
if (currentUser.isVIP) {
order.total *= 0.9;
}
}
// 调用方
applyDiscount(order);
嗯,这里要注意:不是所有参数都适合用查询取代。如果参数的值在每次调用时都可能不同,那就不能这么干。比如折扣比例,今天是9折,明天可能是8折,那就应该作为参数传进来。
以参数取代查询
这个手法和上面那个正好相反。有时候,函数内部直接调用了某个全局函数或服务,导致测试困难、耦合严重。
// 重构前:函数内部硬编码调用
function calculateShippingCost(order) {
const exchangeRate = getExchangeRate(); // 从外部服务获取汇率
return order.weight * exchangeRate;
}
// 测试时,getExchangeRate() 可能返回不稳定值
我曾经在一个金融项目中遇到过类似问题。函数内部直接调用了汇率服务,结果单元测试跑一次一个结果,根本没法断言。后来我改成传参,测试就稳了。
重构后:
// 重构后:把依赖作为参数传入
function calculateShippingCost(order, exchangeRate) {
return order.weight * exchangeRate;
}
// 测试时,可以传入固定值
calculateShippingCost(order, 7.2);
移除设值函数
这个手法针对的是那些"不该被修改"的字段。你想想看,一个对象的字段,如果创建之后就不该变了,那为什么要暴露一个setter?
// 重构前:暴露了不必要的 setter
class Employee {
constructor(name, id) {
this._name = name;
this._id = id;
}
getName() { return this._name; }
setName(name) { this._name = name; } // 这个真的需要吗?
getId() { return this._id; }
setId(id) { this._id = id; } // 员工ID创建后还能改?
}
重构后:
// 重构后:移除不必要的 setter
class Employee {
constructor(name, id) {
this._name = name;
this._id = id;
}
getName() { return this._name; }
getId() { return this._id; }
// 没有 setName 和 setId,创建后不可修改
}
我见过一个系统,所有字段都有getter和setter,美其名曰"统一规范"。结果呢?一个订单的createTime字段,居然有人在业务逻辑里偷偷改了。排查了三天才发现。
changeName代替setName。
知识体系总览
这四个手法其实围绕着一个核心:函数的参数应该精确表达它的依赖。我画了张图帮你理清思路:
实际应用场景
这四个手法在实际项目中经常组合使用。我举个例子:
// 重构前:一团糟
function processOrder(orderId, userId, userType, discountRate, taxRate) {
const user = getUser(userId);
const order = getOrder(orderId);
// 处理逻辑...
}
// 第一步:保持对象完整
function processOrder(order, user, discountRate, taxRate) {
// 处理逻辑...
}
// 第二步:以查询取代参数(user可以从order关联获取)
function processOrder(order, discountRate, taxRate) {
const user = getUser(order.userId);
// 处理逻辑...
}
// 第三步:以参数取代查询(税率依赖外部服务)
function processOrder(order, discountRate, taxRate) {
const user = getUser(order.userId);
// 处理逻辑...
}
// 最终:移除设值函数(order创建后不该改)
class Order {
constructor(id, userId, items) {
this._id = id;
this._userId = userId;
this._items = items;
}
getId() { return this._id; }
getUserId() { return this._userId; }
getItems() { return this._items; }
// 没有 setId, setUserId, setItems
}
你看,经过几轮重构,函数的参数从5个减少到3个,而且每个参数都有明确的语义。调用方也更轻松了。
好了,这一节的内容就这些。记住:好的API设计,是让调用方少操心,让实现方少耦合。下次你写函数时,多想想——这个参数真的需要调用方传吗?这个字段真的需要暴露setter吗?想清楚了,代码质量自然就上去了。