基本重构手法(一):提炼函数、内联函数、提炼变量、内联变量

重构这件事,说白了就是「整理代码」。就像你收拾房间,有些东西要拿出来单独放,有些东西要合并到一起。今天要讲的这四种手法,是重构里最基础、也最常用的操作。我做了这么多年项目,几乎每天都会用到它们。

提炼函数:把一段逻辑「拎」出来

提炼函数,就是把一段有明确目的的代码,单独抽成一个函数。为什么要这么做?因为代码的可读性会大幅提升。

举个例子,你可能会写出这样的代码:

function printOwing(invoice) {
  let outstanding = 0;

  // 计算未付金额
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }

  // 打印账单
  console.log(`客户:${invoice.customer}`);
  console.log(`金额:${outstanding}`);
}

这段代码里,计算和打印混在一起。我建议把计算部分提炼出来:

function printOwing(invoice) {
  const outstanding = calculateOutstanding(invoice);
  printBill(invoice, outstanding);
}

function calculateOutstanding(invoice) {
  let result = 0;
  for (const o of invoice.orders) {
    result += o.amount;
  }
  return result;
}

function printBill(invoice, outstanding) {
  console.log(`客户:${invoice.customer}`);
  console.log(`金额:${outstanding}`);
}

你看,主函数变得非常清晰。它只负责「做什么」,不关心「怎么做」。

我的习惯:提炼函数时,函数名要是一个动词短语。比如 calculateOutstandingprintBill。这样读代码的人一眼就知道这个函数在干什么。

内联函数:把多余的「壳」去掉

内联函数和提炼函数正好相反。当一个函数太简单,或者它的逻辑已经不言自明时,我会把它内联回去。

我在项目中遇到过这样的情况:有人写了一个函数,里面只有一行代码,而且函数名和实现几乎一样。比如:

function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}

这个 moreThanFiveLateDeliveries 函数,其实没有提供额外的信息。直接内联掉:

function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

代码反而更短、更直接。你想想看,如果每个函数都只有一行,那还不如直接写出来。

注意:内联函数不是「删代码」,而是「消除不必要的间接层」。如果一个函数被多处调用,或者未来可能扩展,那就不要内联。

提炼变量:给表达式一个名字

复杂的表达式很难读懂。提炼变量就是给这些表达式起个名字,让代码自解释。

比如这段代码:

if (order.price * order.quantity > 1000) {
  // 处理大额订单
}

这个条件判断是什么意思?1000 是什么?提炼变量后:

const isLargeOrder = order.price * order.quantity > 1000;

if (isLargeOrder) {
  // 处理大额订单
}

现在代码自己会说话。你不需要注释,就能明白这段逻辑。

我曾经在一个遗留系统里看到过这样的代码:

if (user.role === 'admin' && user.status === 'active' && user.lastLogin > Date.now() - 86400000) {
  // 允许访问
}

86400000 是什么?是 24 小时的毫秒数。我提炼了三个变量:

const isAdmin = user.role === 'admin';
const isActive = user.status === 'active';
const isRecentLogin = user.lastLogin > Date.now() - ONE_DAY_MS;

if (isAdmin && isActive && isRecentLogin) {
  // 允许访问
}

现在这段代码,任何人都能一眼看懂。

核心原则:如果一个表达式需要你停下来思考,那就提炼成变量。好的代码不需要注释,它自己就是文档。

内联变量:去掉不必要的中间人

内联变量是提炼变量的逆操作。当一个变量名没有提供额外信息,或者只被使用一次时,我会把它内联掉。

举个例子:

const basePrice = order.basePrice;
const finalPrice = basePrice * (1 - order.discount);

return finalPrice;

这里的 basePricefinalPrice 都只使用了一次。直接内联:

return order.basePrice * (1 - order.discount);

代码更紧凑,而且没有丢失任何信息。你想想看,如果每个中间结果都用一个变量保存,代码会变得又长又啰嗦。

我的判断标准:如果变量名和表达式本身一样长,或者变量只在一个地方使用,那就内联。反之,如果变量名提供了额外的语义,就保留。

四种手法的关系

这四种手法其实是两对「反向操作」:

  • 提炼函数 ↔ 内联函数:一个把代码抽出来,一个把代码放回去
  • 提炼变量 ↔ 内联变量:一个给表达式命名,一个去掉不必要的名字

它们不是非此即彼的选择。在实际项目中,我会根据代码的上下文来决定用哪一种。比如,一个函数如果越来越复杂,我就提炼;如果越来越简单,我就内联。

四种基本重构手法关系图 提炼函数 内联函数 提炼变量 内联变量 反向操作 反向操作 提炼:增加可读性 | 内联:减少间接层

什么时候用哪种?

我总结了一个简单的判断逻辑:

场景 推荐手法 原因
一段代码有明确目的,且被注释标记 提炼函数 用函数名代替注释
一个函数只有一行,且调用处很少 内联函数 消除不必要的间接层
复杂表达式,难以一眼看懂 提炼变量 给表达式起个有意义的名字
变量名和表达式含义重复 内联变量 去掉多余的中间人
避坑指南:我曾经在一个项目里过度提炼,把代码拆成了几十个只有一两行的小函数。结果代码变得支离破碎,读起来反而更费劲。后来我学会了:提炼是为了可读性,不是为了「看起来整洁」。如果提炼后代码更难懂了,那就不要提炼。

嗯,这四种手法其实很简单,但用好了能大幅提升代码质量。我建议你在日常开发中多练习,慢慢就会形成直觉——看到一段代码,就知道该提炼还是该内联。

记住一句话:好的代码,读起来像是一篇好文章。每个函数、每个变量,都应该有它存在的理由。