3、基本重构手法(上):提取函数、内联函数、提取变量、内联变量

好,咱们正式开始动手了。

重构这件事,说白了就是「整理代码」。就像你收拾房间,先把东西归类,再把不用的扔掉。今天要讲的四个手法,就是最基础的「归类」和「精简」操作。我个人习惯把它们称为重构的「四则运算」——加减乘除的基础,后面所有复杂手法都从这里衍生。

3.1 提取函数(Extract Function)

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

什么叫提取函数?就是把一段逻辑独立的代码块,从原来的函数里「拎」出来,放到一个新函数里。为什么要这么做?因为代码的可读性。你想想看,一个函数如果超过20行,读起来就开始费劲了。超过50行,基本就是一团乱麻。

核心原则: 一段代码如果有一个清晰的「意图」,就应该被提取成一个函数。

举个例子,假设你有一段代码在计算订单的总价,同时还要处理折扣和税费:

// 重构前
function calculateTotal(order) {
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  // 应用折扣
  if (order.customer.level === 'vip') {
    total *= 0.9;
  } else if (order.customer.level === 'gold') {
    total *= 0.95;
  }
  // 添加税费
  total *= 1.08;
  return total;
}

这段代码其实做了三件事:计算小计、应用折扣、添加税费。每一件事都可以独立提取。我建议这样改:

// 重构后
function calculateTotal(order) {
  const subtotal = calculateSubtotal(order.items);
  const discounted = applyDiscount(subtotal, order.customer);
  return addTax(discounted);
}

function calculateSubtotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

function applyDiscount(amount, customer) {
  if (customer.level === 'vip') return amount * 0.9;
  if (customer.level === 'gold') return amount * 0.95;
  return amount;
}

function addTax(amount) {
  return amount * 1.08;
}

你看,主函数现在只有三行,每一行都在说「我在做什么」,而不是「我怎么做的」。这就是提取函数的核心价值——把「怎么做」藏起来,把「做什么」露出来。

我的小技巧: 提取函数时,函数名一定要用动词开头。比如 calculateSubtotal、applyDiscount。别用 subtotalCalculation 这种名词形式,读起来别扭。

3.2 内联函数(Inline Function)

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

什么时候需要内联?当一个函数本身太简单,或者它的实现比它的名字还要清晰时,就应该把它「揉回去」。我在项目中遇到过一种情况:有些同事喜欢把所有逻辑都拆成函数,结果一个简单的赋值操作也要包一层函数。这就过度设计了。

// 重构前 - 过度封装
function getBasePrice(price) {
  return price;
}

function calculateTotal(price, quantity) {
  return getBasePrice(price) * quantity;
}

getBasePrice 这个函数什么都没做,只是返回了传入的值。这种函数就是内联的对象:

// 重构后
function calculateTotal(price, quantity) {
  return price * quantity;
}
注意: 内联函数不是让你把所有代码都塞回一个函数里。它的目的是消除「不必要的间接层」。如果一个函数的存在反而增加了理解成本,那就内联它。

我曾经在一个遗留系统里看到过一个函数,叫 getValue(),里面只有一行 return this.value;。整个项目里被调用了300多次。你说这个函数有意义吗?没有。它只是增加了一次跳转。我花了一个下午,把所有调用点都替换成了直接访问 this.value,代码量减少了,可读性反而提升了。

3.3 提取变量(Extract Variable)

提取变量,就是把一个复杂的表达式「命名」成一个变量。说白了,就是给一段计算结果起个名字。

为什么要这么做?因为人脑处理复杂表达式的能力有限。你想想看,下面这段代码你能一眼看出它在算什么吗?

// 重构前
if (order.items.length > 0 && order.customer.level === 'vip' && order.total > 1000) {
  // 执行特殊逻辑
}

三个条件挤在一起,读起来很累。提取变量后:

// 重构后
const hasItems = order.items.length > 0;
const isVip = order.customer.level === 'vip';
const isLargeOrder = order.total > 1000;

if (hasItems && isVip && isLargeOrder) {
  // 执行特殊逻辑
}

现在条件变成了三个有名字的布尔变量,读起来就像在说「如果有商品、是VIP、且是大订单」。嗯,这才是人话。

提取变量的时机:
  • 表达式在代码中重复出现(比如同一个计算出现多次)
  • 表达式太长,一眼看不完(超过80个字符)
  • 表达式逻辑复杂,需要注释才能理解(用变量名代替注释)

3.4 内联变量(Inline Variable)

内联变量是提取变量的逆操作。当一个变量的名字还不如表达式本身清晰时,就应该内联它。

举个例子:

// 重构前
const basePrice = price * quantity;
const finalPrice = basePrice * 1.08;
return finalPrice;

这里的 basePrice 和 finalPrice 其实没有提供额外信息。直接内联:

// 重构后
return price * quantity * 1.08;

你看,代码反而更简洁了。内联变量的核心判断标准是:这个变量名是否比它的值更容易理解? 如果是,保留;如果不是,内联。

我的经验: 内联变量最常见的场景是「临时变量滥用」。有些开发者喜欢把每一步计算结果都存成变量,结果一个函数里出现十几个临时变量。这时候,大胆内联那些「一次使用」的变量,代码会清爽很多。

3.5 四个手法的关系

这四个手法其实是两对「镜像操作」:

操作 正向 逆向
函数层面 提取函数(拆) 内联函数(合)
变量层面 提取变量(命名) 内联变量(简化)

它们不是非此即彼的关系。在实际项目中,你经常需要组合使用。比如先提取变量,发现变量名不好起,再内联回去;或者先提取函数,发现提取出来的函数太简单,再内联回去。这都是正常的。

避坑指南: 我曾经在一个项目里,看到有人把提取函数和内联函数反复做了三遍。为什么?因为他没有想清楚「这个函数的职责到底是什么」。记住:重构不是机械操作,每一次提取或内联,都要问自己「这样做是否让代码更容易理解?」。如果答案是否定的,就停下来。

3.6 实战建议

这四个手法,我建议你从「提取变量」开始练习。因为它风险最低,效果最明显。你只需要给复杂表达式起个名字,代码可读性就能提升一大截。

然后是「提取函数」。这个需要一点经验来判断「多大的代码块适合提取」。我的经验是:如果一个代码块需要写注释来解释它在做什么,那就提取成函数,用函数名代替注释。

至于内联操作,它们更像是「清理工具」。当你发现代码中有不必要的间接层时,就用内联把它们「拍平」。

记住一句话: 好的代码,读起来像是一篇好文章。有清晰的段落(函数),有明确的术语(变量名),没有多余的废话(内联)。

嗯,今天就到这里。下一章我们会继续讲剩下的基本重构手法——移动语句、拆分循环、合并条件表达式等。这些手法组合起来,基本能覆盖日常开发中80%的重构场景。