6、第一组重构(上):提炼函数、内联函数、提炼变量、内联变量

好,咱们正式开始讲重构手法。

第一组重构,我选了四个最基础、也最常用的操作:提炼函数、内联函数、提炼变量、内联变量。说白了,它们就是代码整理的“四把扳手”。你写代码时,觉得某块逻辑拧巴了,拿这四把扳手拧一拧,代码立马顺眼很多。

我个人习惯,每次接手一个老项目,第一件事不是读全部代码,而是先扫一遍长函数和复杂表达式。看到超过20行的函数,我就开始琢磨:这里能不能提炼一下?看到那种嵌套三层的三元表达式,我就想:这不得拆成变量?

嗯,咱们一个一个来。

提炼函数(Extract Function)

这是最常用的重构手法,没有之一。

它的核心思想很简单:把一段独立的逻辑,从一个大函数里抽出来,变成一个单独的函数。为什么要这么做?因为函数名本身就是注释。你想想看,一段代码如果被包在一个叫 calculateInvoiceTotal 的函数里,别人一看就知道它在算什么。但如果这段逻辑直接写在主流程里,别人得一行一行读才能明白。

核心原则:一个函数只做一件事,并且把这件事的名字写在函数名上。

我在项目中遇到过一段代码,一个函数写了300多行,里面混杂着数据校验、格式转换、网络请求、UI更新。我当时花了整整一个下午,把它拆成了12个小函数。拆完之后,原来那个大函数只剩下了20行——就是按顺序调用这12个函数。后来接手的人说,这是他见过最好读的代码。

来看个例子。重构前:

function printOwing(invoice) {
  let outstanding = 0;

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

  // 记录到期日
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

  // 打印详情
  console.log(`客户:${invoice.customer}`);
  console.log(`金额:${outstanding}`);
  console.log(`到期日:${invoice.dueDate.toLocaleDateString()}`);
}

重构后:

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

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

function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails(invoice, outstanding) {
  console.log(`客户:${invoice.customer}`);
  console.log(`金额:${outstanding}`);
  console.log(`到期日:${invoice.dueDate.toLocaleDateString()}`);
}

看到了吗?原来的注释变成了函数名。代码量没少,但可读性翻倍。

小技巧:提炼函数时,如果新函数需要多个参数,说明你可能没拆对。理想情况下,新函数应该只需要0-2个参数。

内联函数(Inline Function)

有提炼就有内联。内联函数是提炼函数的逆操作。

什么时候需要内联?当函数体本身比函数名还简单的时候。说白了,如果函数名和函数体一样长,那这个函数就是多余的。

我曾经在一个项目里看到这样的代码:

function isLate(deliveryDate) {
  return deliveryDate < new Date();
}

// 调用处
if (isLate(order.deliveryDate)) {
  // ...
}

这个函数有必要存在吗?我个人觉得没必要。deliveryDate < new Date() 本身就已经很直白了。内联之后:

if (order.deliveryDate < new Date()) {
  // ...
}

代码反而更短、更直接。

注意:内联函数只适用于那些“一眼就能看懂”的简单逻辑。如果函数内部有复杂的业务规则,或者函数被多处调用,请保留它。

提炼变量(Extract Variable)

提炼变量,也叫“引入解释性变量”。

它的场景通常是:一个表达式太长了,或者嵌套太深了,读起来费劲。这时候,把中间结果赋给一个变量,用变量名来解释这段逻辑的含义。

举个例子,重构前:

if (order.quantity > 100 && order.price > 500 && order.customer.isVIP) {
  // 给大客户打折
}

重构后:

const isBigOrder = order.quantity > 100 && order.price > 500;
const isVIP = order.customer.isVIP;

if (isBigOrder && isVIP) {
  // 给大客户打折
}

你想想看,哪个更容易理解?显然是后者。变量名 isBigOrderisVIP 直接告诉了你条件判断的意图。

我在项目中遇到过最夸张的一个表达式,大概有80个字符长,里面混着三四个逻辑运算符。我当时盯着看了五分钟才搞明白它在算什么。后来我把它拆成了5个变量,每个变量都起了有意义的名字。从那以后,团队就定了个规矩:任何超过40个字符的表达式,必须提炼变量。

经验之谈:提炼变量不只是为了缩短代码,更是为了给代码“加注释”。变量名就是最好的注释。

内联变量(Inline Variable)

内联变量是提炼变量的逆操作。

什么时候用?当变量名不比表达式本身更清晰的时候。说白了,如果变量名没有提供额外信息,那就别用变量。

比如:

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

这里的 basePrice 变量其实没什么必要,直接写成 order.basePrice * (1 - discountRate) 反而更直接。内联之后:

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

少了一行代码,少了一个变量,逻辑反而更紧凑。

判断标准:问自己一个问题——这个变量名是否提供了表达式本身没有的信息?如果答案是否定的,就内联它。

知识体系总览

下面这张图,把这四个重构手法的关系梳理了一下。你可以把它当作一个快速决策指南:

第一组重构:四把扳手 代码可读性 提炼函数(Extract Function) 提炼变量(Extract Variable) 内联函数(Inline Function) 内联变量(Inline Variable) 提炼:把复杂逻辑拆成有名字的单元 内联:去掉不必要的间接层

这张图想表达的核心意思是:提炼和内联是一对互补操作。提炼让代码更清晰,内联让代码更简洁。你需要根据实际情况,在两者之间找到平衡。

避坑指南

最后,分享几个我踩过的坑:

  • 不要过度提炼。我曾经把一个项目拆得太碎,每个函数只有两三行。结果代码量翻了一倍,读起来反而更累。记住,提炼的目的是可读性,不是追求函数数量。
  • 内联前先确认调用点。如果函数被多处调用,内联会导致大量重复代码。这时候应该保留函数,甚至考虑用“搬移函数”来优化。
  • 变量名要起好。提炼变量时,别用 tempdataresult 这种名字。变量名应该准确描述这段逻辑的业务含义。
  • 先写测试再重构。这是铁律。没有测试覆盖的代码,重构就是在走钢丝。我吃过这个亏,现在每次重构前必写测试。

好了,这四种手法讲完了。它们看起来简单,但用好了,代码质量能提升一大截。下一组重构,咱们接着聊变量改名、引入参数对象等更进阶的操作。


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