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,创建后不可修改
}

我见过一个系统,所有字段都有gettersetter,美其名曰"统一规范"。结果呢?一个订单的createTime字段,居然有人在业务逻辑里偷偷改了。排查了三天才发现。

判断标准:如果一个字段在对象创建后就不应该变化,那就移除它的设值函数。如果确实需要修改,考虑用"改变函数声明"来明确语义,比如用changeName代替setName

知识体系总览

这四个手法其实围绕着一个核心:函数的参数应该精确表达它的依赖。我画了张图帮你理清思路:

API重构(下)核心手法 函数参数设计 保持对象完整 传对象,不拆字段 以查询取代参数 内部能查就别传 以参数取代查询 外部依赖传进来 移除设值函数 不该改的别暴露 核心决策逻辑 参数能否从函数内部稳定获取? → 能 → 以查询取代参数 → 不能 → 是否需要外部依赖? → 是 → 以参数取代查询 函数用到对象多个字段? → 是 → 保持对象完整 字段创建后不该变? → 是 → 移除设值函数

实际应用场景

这四个手法在实际项目中经常组合使用。我举个例子:

// 重构前:一团糟
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时,别想着一步到位。先看看哪些参数可以合并(保持对象完整),再看看哪些参数可以内部获取(以查询取代参数),最后检查哪些内部调用应该暴露为参数(以参数取代查询)。三步走完,函数签名就清爽多了。

好了,这一节的内容就这些。记住:好的API设计,是让调用方少操心,让实现方少耦合。下次你写函数时,多想想——这个参数真的需要调用方传吗?这个字段真的需要暴露setter吗?想清楚了,代码质量自然就上去了。


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