基本重构手法(二):改变函数声明、封装变量、变量改名、引入参数对象

这一讲我们继续聊重构的基本手法。说实话,这些手法看起来简单,但恰恰是日常开发中用得最多的。我见过太多代码,就是因为这些基础手法没用到位,慢慢变得难以维护。

今天要讲的四个手法,说白了就是帮我们把代码的「命名」和「结构」理清楚。你想想看,代码里最常打交道的是什么?无非就是函数、变量、参数这三样东西。把它们管好了,代码质量就上去了一大半。

改变函数声明

函数声明,就是函数的「门面」。名字叫什么?参数有哪些?返回值是什么?这些决定了别人怎么用你的函数。

我在项目中遇到过一种情况:一个函数叫 processData,传进去三个参数,但实际只用了两个。第三个参数是早期版本遗留的,后来业务变了,这个参数就没人用了。但大家都不敢删,怕影响调用方。结果呢?每个调用方都得传一个 null 进去。

这就是典型的「函数声明需要改变」的信号。

核心原则: 函数声明应该清晰表达函数的意图。如果做不到,就改。

具体怎么做?我总结了几种常见场景:

1. 改名

函数名不好,直接改。别犹豫。

// 改名前
function calc(a, b) {
  return a * b + a;
}

// 改名后
function calculateTotalPrice(unitPrice, quantity) {
  return unitPrice * quantity + unitPrice;
}

嗯,这里要注意:改名后要全局搜索所有调用处,全部替换。IDE 的重构功能可以帮你做这件事。

2. 增减参数

参数多了少了,都要调整。但别一次加太多参数,超过三个就要考虑用对象了。

// 参数太多,调用方痛苦
function createUser(name, age, email, phone, address, city, zipCode) {
  // ...
}

// 改成参数对象
function createUser(userInfo) {
  // userInfo.name, userInfo.age ...
}

3. 改变返回值

有时候函数返回了太多东西,调用方只用了其中一部分。这时候可以考虑拆分函数,或者返回一个对象。

我的习惯: 每次修改函数声明后,立即运行测试。确保没有破坏任何调用方。

封装变量

变量,尤其是全局变量或模块级变量,是代码里的「定时炸弹」。你永远不知道谁在什么时候改了它。

我曾经在一个遗留系统里看到这样的代码:一个全局的 config 对象,被二十多个文件直接读写。有一天某个同事不小心把 config.timeout 写成了 config.timeOut,结果系统间歇性超时,查了两天才找到原因。

封装变量,就是把直接暴露的变量藏起来,通过函数来访问和修改。

// 封装前
let defaultTimeout = 3000;

// 封装后
let _defaultTimeout = 3000;

export function getDefaultTimeout() {
  return _defaultTimeout;
}

export function setDefaultTimeout(value) {
  if (value < 0) {
    throw new Error('超时时间不能为负数');
  }
  _defaultTimeout = value;
}

这样做的好处很明显:

  • 可以加校验逻辑(比如上面的负数检查)
  • 可以加日志,知道谁在什么时候改了值
  • 未来可以改变内部实现,不影响外部调用
注意: 不要过度封装。如果一个变量只在函数内部使用,就没必要封装。封装的目标是「跨作用域」的变量。

变量改名

变量改名,听起来最简单,但做起来最考验功力。为什么?因为一个好名字,要准确表达变量的「含义」和「用途」。

我见过这样的代码:

let d = new Date();
let t = d.getTime();
let r = t - 86400000;

你能一眼看出 r 是什么吗?86400000 又是什么?

改成这样就好多了:

let now = new Date();
let currentTimestamp = now.getTime();
let yesterdayTimestamp = currentTimestamp - ONE_DAY_IN_MILLISECONDS;

变量改名的几个原则:

  • 长度不是问题,清晰才是。 别怕名字长,怕的是看不懂。
  • 用名词表示数据,用动词表示函数。 userList 是数据,getUserList 是操作。
  • 避免歧义。 datainfotemp 这类名字,能不用就不用。
一个小技巧: 如果你写代码时想不出好名字,说明你对这段逻辑的理解还不够深。先写注释,再根据注释提炼名字。

引入参数对象

这个手法,说白了就是把一组经常一起出现的参数,封装成一个对象。

你想想看,如果一个函数有五个参数,调用的时候你得记住每个参数的顺序和类型。这多痛苦?

// 重构前
function bookFlight(departure, destination, date, passengers, classType) {
  // ...
}

// 调用
bookFlight('北京', '上海', '2024-03-15', 2, '经济舱');

// 重构后
function bookFlight(bookingInfo) {
  // bookingInfo.departure, bookingInfo.destination ...
}

// 调用
bookFlight({
  departure: '北京',
  destination: '上海',
  date: '2024-03-15',
  passengers: 2,
  classType: '经济舱'
});

引入参数对象的好处:

  1. 调用方不用记参数顺序。 按名字传参,不容易出错。
  2. 新增参数不影响现有调用。 在对象里加个字段就行,不用改所有调用处。
  3. 可以复用这个对象。 如果多个函数都用同一组参数,直接复用这个对象类型。
什么时候该用? 当函数参数超过 3 个,或者多个函数共享同一组参数时,就该考虑引入参数对象了。

知识体系总览

下面这张图,把这四个手法的关系梳理了一下。你可以看到,它们都围绕着「代码的可读性和可维护性」这个核心目标。

基本重构手法(二) 改变函数声明 封装变量 变量改名 引入参数对象 改名 · 增减参数 · 改返回值 隐藏实现 · 加校验 · 加日志 清晰 · 无歧义 · 表意准确 减少参数 · 提高可读性 · 便于复用 核心目标:提升代码的可读性与可维护性

综合案例:一个订单处理函数

最后,我们看一个综合案例。假设你接手了这样一段代码:

function process(o, u, p) {
  let t = o.total;
  let d = o.discount;
  let f = t * d;
  let r = u.balance - f;
  if (r >= 0) {
    u.balance = r;
    p.push(o);
    return true;
  }
  return false;
}

这段代码的问题太多了:变量名看不懂、参数含义不明、逻辑混在一起。我们用今天学的手法来重构:

// 第一步:变量改名
function processOrder(order, user, processedOrders) {
  let total = order.total;
  let discount = order.discount;
  let finalPrice = total * discount;
  let remainingBalance = user.balance - finalPrice;
  
  if (remainingBalance >= 0) {
    user.balance = remainingBalance;
    processedOrders.push(order);
    return true;
  }
  return false;
}

// 第二步:引入参数对象(如果参数继续增多)
function processOrder(orderInfo) {
  const { order, user, processedOrders } = orderInfo;
  // ... 逻辑不变
}

// 第三步:封装变量(如果 user.balance 是全局可变的)
function deductBalance(user, amount) {
  if (user.balance < amount) {
    return false;
  }
  user.balance -= amount;
  return true;
}

你看,经过这几个手法的组合使用,代码变得清晰多了。每个函数只做一件事,每个变量名都表达了它的含义。

我的建议: 重构不是一蹴而就的。每次改一点,跑测试,再改一点。慢慢来,比较快。

好了,这一讲就到这里。这四个手法虽然基础,但用好了,能让你的代码质量上一个台阶。下次写代码时,不妨多想想:这个函数名够不够好?这个变量能不能封装?参数是不是太多了?


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