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

重构这玩意儿,说白了就是给代码"洗澡"。代码写久了,总会沾上一些"泥巴"——逻辑太长的函数、意义不明的变量、绕来绕去的调用链。今天这组重构手法,是我在日常开发中用得最频繁的四个:提炼函数、内联函数、提炼变量、内联变量。

它们就像代码的"四则运算",简单但极其重要。我习惯把它们放在重构工具箱的最上层,因为几乎每次重构都会用到它们。

提炼函数(Extract Function)

这是我最喜欢的重构手法,没有之一。

你想想看,一个函数如果超过20行,读起来就开始费劲了。如果超过50行,基本就是在考验读者的耐心。我见过最夸张的一个函数有800多行,那哥们儿把整个业务逻辑都塞进了一个方法里——嗯,后来他离职了,接手的人哭了三个月。

提炼函数的核心思想很简单:把一段独立的逻辑从大函数里抽出来,变成一个新函数

什么时候该提炼?

  • 一段代码有明确的"目的"(比如"计算折扣"、"验证用户权限")
  • 一段代码被注释包裹(注释往往说明这段逻辑值得独立)
  • 一段代码在多个地方重复出现
  • 函数太长,一眼看不到底

看个例子。重构前:

function printOwing(invoice) {
  let outstanding = 0;

  console.log("***********************");
  console.log("**** Customer Owes ****");
  console.log("***********************");

  // 计算未付金额
  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(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

重构后:

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

function printBanner() {
  console.log("***********************");
  console.log("**** Customer Owes ****");
  console.log("***********************");
}

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(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

你看,原来的函数有四个职责:打印横幅、计算金额、记录日期、打印详情。现在每个函数只做一件事,名字就是它的文档。

我的习惯:给新函数取名时,多用动词+名词的结构,比如 calculateOutstandingvalidateInput。别用 doSomething 这种名字——那等于没取名。

内联函数(Inline Function)

有提炼就有内联。内联函数是提炼函数的"反向操作"。

什么时候需要内联?当函数本身太简单,简单到它的名字比它的实现还长的时候。我在项目中遇到过这样的情况:有人写了一个函数叫 isTrue(),里面就一行 return value === true。这种函数,说白了就是脱裤子放屁。

重构前:

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

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

重构后:

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

内联的另一个场景是"过度抽象"。有些团队喜欢把每个操作都封装成函数,结果代码里全是跳转,读起来像在玩迷宫游戏。这时候内联反而能让代码更清晰。

注意:内联函数不是让你把所有代码都塞回一个函数里。它的目的是消除那些"不必要的间接层"。如果一个函数被多处调用,或者逻辑本身很复杂,就别内联了。

提炼变量(Extract Variable)

提炼变量,也叫"引入解释性变量"。说白了,就是给复杂的表达式起个名字。

我见过最离谱的代码是这样的:

return (order.quantity * order.itemPrice) - 
       Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
       Math.min(order.quantity * order.itemPrice * 0.1, 100);

你告诉我这行代码在算什么?反正我第一次看的时候,愣是盯了五分钟才反应过来——哦,原来是在算订单总价,包含折扣和运费。

重构后:

const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;

现在一目了然:基础价格、数量折扣、运费,三个变量清清楚楚。

提炼变量的时机:

  • 表达式太长,一眼看不懂
  • 同一个表达式在多个地方出现(这时候提炼变量还能消除重复)
  • 表达式的结果有明确的业务含义

内联变量(Inline Variable)

内联变量是提炼变量的反向操作。当变量名不比表达式本身更清晰时,就该内联了。

举个例子:

const isEligible = customer.age > 18 && customer.creditScore > 700;
if (isEligible) {
  // ...
}

这个 isEligible 变量其实挺清晰的,保留没问题。但如果是这样:

const x = customer.age > 18 && customer.creditScore > 700;
if (x) {
  // ...
}

这个 x 是什么鬼?还不如直接写条件表达式呢。

内联后:

if (customer.age > 18 && customer.creditScore > 700) {
  // ...
}

我曾经在一个项目里看到有人给每个中间结果都起了变量名,结果一个函数里堆了十几个变量,读起来比看表达式还累。这时候就该果断内联。

我的建议:变量名如果比表达式还难懂,或者变量只在一个地方使用,那就内联掉。别为了"变量"而"变量"。

知识体系总览

这四种重构手法其实构成了一个"双向通道":

第一组重构:提炼与内联 函数层面 提炼函数:大函数 → 小函数 内联函数:小函数 → 大函数 核心:职责单一,命名清晰 变量层面 提炼变量:表达式 → 变量 内联变量:变量 → 表达式 核心:消除重复,增强可读性 相互配合 灵活运用 决策指南 🔹 代码有明确目的? → 提炼函数 🔹 函数太简单? → 内联函数 🔹 表达式太复杂? → 提炼变量 🔹 变量名不如表达式? → 内联变量 记住:重构不是炫技,而是让代码更容易被理解

这四种手法,说白了就是"拆"和"合"的艺术。拆得太碎,代码像散沙;合得太紧,代码像铁板。好的代码,就是在拆合之间找到平衡。

我个人习惯是:先提炼,再内联。先拆出有意义的函数和变量,然后回头看,如果发现某些提炼过度了,再内联回去。重构不是一次性的,它是个反复打磨的过程。

避坑指南:我曾经在一个项目里过度提炼,把每个if条件都提炼成了函数,结果代码里全是 isXxx()hasYyy(),读起来反而更费劲。后来我学乖了:提炼之前先问自己一句——"这个提炼真的让代码更好读了吗?"

嗯,这四种手法就先聊到这儿。它们是重构的"基本功",就像练武要先扎马步一样。别嫌简单,真正用好它们,你的代码质量会上一个台阶。

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