重构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; }
}

你想想看,setNamesetAge 真的需要暴露吗?如果用户的姓名和年龄在创建后就不应该改变,那为什么要提供 setter?

// 重构后
class User {
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  
  getName() { return this._name; }
  getAge() { return this._age; }
}

移除设值函数的好处很明显:

  • 不可变性:对象一旦创建,状态就不会变。这能避免很多奇怪的 bug。
  • 意图清晰:看到没有 setter,别人就知道这个字段是只读的。
  • 减少耦合:外部不能随意修改内部状态,所有修改都必须通过方法。
避坑指南:我曾经在一个项目中,把所有 setter 都去掉了。结果发现有些字段确实需要在运行时修改,比如用户的最后登录时间。这时候不要硬来,该保留的 setter 还是要保留。关键是要问自己:这个字段真的需要被外部修改吗?

知识体系总览

这一章讲的四个手法,其实都在解决同一个问题:参数太多了怎么办? 我画了一张图,帮你理清思路:

API重构(下):参数优化四手法 参数太多怎么办? 保持对象完整 多个字段来自同一对象 → 传整个对象 以查询取代参数 参数可内部获取 → 去掉参数 以参数取代查询 查询有副作用/很慢 → 改为参数 移除设值函数 字段创建后不变 → 去掉 setter 核心原则 参数越少越好,能省则省。但不要为了省参数而牺牲代码的清晰度。

总结一下

这一章我们聊了四个手法:

手法 适用场景 核心思想
保持对象完整 方法内部使用对象的多个字段 传整个对象,别拆开传
以查询取代参数 参数值可以在方法内部获取 去掉参数,内部查询
以参数取代查询 查询有副作用或很慢 改为参数,让调用方控制
移除设值函数 字段创建后不应修改 去掉 setter,保持不可变

说白了,这些手法都是在帮你做减法。参数少了,代码就简单了。代码简单了,bug 就少了。bug 少了,你就能早点下班了。

嗯,今天就到这里。下一章我们聊重构中的“处理继承关系”,那又是一个大坑。


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