7、基本重构手法(上):提炼函数、内联函数、提炼变量、内联变量、改变函数声明、封装变量

重构这件事,说白了就是「在不改变外部行为的前提下,改善内部结构」。听起来简单,做起来全是细节。我这些年带团队,发现很多同学不是不想重构,而是不知道从哪下手。今天咱们就把最基础的六种手法掰开揉碎讲清楚。

这六种手法,就像木匠手里的刨子、凿子、锤子。单独看都不复杂,但组合起来能解决大部分代码坏味道。我个人习惯把它们分成两组:一组是「提炼」和「内联」的对称操作,另一组是「改变声明」和「封装变量」的结构调整。

基本重构手法(上)知识体系 六种基本重构手法 对称操作:提炼 vs 内联 提炼函数 内联函数 提炼变量 内联变量 结构调整:声明与封装 改变函数声明 封装变量 核心原则:小步提交 · 保持可运行 · 测试覆盖

提炼函数:把意图从实现中解放出来

提炼函数是最基础、最常用的重构手法。它的核心逻辑很简单:当你看到一段代码需要注释才能看懂时,就该把它提炼成一个函数。

我在项目中遇到过最典型的场景:一个函数写了300行,里面混杂着数据校验、业务计算、日志输出。每次改需求都提心吊胆。后来我强制自己:如果一个函数超过20行,就停下来想想能不能拆。

提炼函数的关键判断标准

  • 代码片段有明确的「做什么」的意图
  • 片段内部存在重复逻辑
  • 片段与周围代码的耦合度低
  • 片段长度超过5-10行

举个例子,看看这段代码:

// 重构前
function printOwing(invoice) {
  let outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  console.log(`客户:${invoice.customer}`);
  console.log(`欠款:${outstanding}`);
  console.log(`日期:${new Date().toLocaleDateString()}`);
}

这段代码干了三件事:计算欠款、打印客户信息、打印日期。咱们把计算和打印分开:

// 重构后
function printOwing(invoice) {
  const outstanding = calculateOutstanding(invoice);
  printDetails(invoice, outstanding);
}

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

function printDetails(invoice, outstanding) {
  console.log(`客户:${invoice.customer}`);
  console.log(`欠款:${outstanding}`);
  console.log(`日期:${new Date().toLocaleDateString()}`);
}

我的小技巧:提炼函数时,先想好函数名。如果名字很难起,说明这段代码可能「职责不单一」。我曾经花10分钟想一个函数名,最后发现应该拆成两个函数。

内联函数:当间接层变成负担

有提炼就有内联。内联函数是提炼函数的逆操作。什么时候需要内联?当函数体比函数名更容易理解时。

你想想看,有些函数就是一行代码,比如:

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

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

这种间接层完全没有必要。直接内联:

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

注意:内联函数不是「删代码」,而是「消除不必要的间接层」。我曾经见过有人把内联理解成「把所有函数都写在一起」,结果搞出一个500行的怪物函数。内联的前提是:函数本身已经足够简单,不需要名字来解释。

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

提炼变量和提炼函数思路类似,只不过对象是表达式而不是语句块。当表达式太复杂时,给它一个名字,代码瞬间就清晰了。

我记得有一次review代码,看到这样的表达式:

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

提炼变量后:

const basePrice = order.price * order.quantity;
const finalPrice = basePrice - order.discount;
const isLargeOrder = finalPrice > 1000;

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

嗯,这里要注意:提炼变量不是越多越好。如果一个变量只使用一次,而且表达式本身很简单,那就没必要提炼。我见过有人把 const a = 1 + 2 也提炼成变量,这就过度了。

内联变量:去掉多余的中间人

内联变量是提炼变量的逆操作。当变量名不比表达式本身更清晰时,就把它内联掉。

举个例子:

const basePrice = order.basePrice;
const quantity = order.quantity;
const total = basePrice * quantity;

如果 basePricequantity 只使用一次,直接内联更简洁:

const total = order.basePrice * order.quantity;

什么时候该内联变量?

  • 变量只使用一次
  • 变量名没有提供额外信息
  • 表达式本身很简短

改变函数声明:改名、改参数、改签名

函数声明是函数的「门面」。一个好的函数名,胜过千言万语。改变函数声明包括:改名、增删参数、改变参数顺序。

我见过最糟糕的函数名是 processData() —— 它几乎等于什么都没说。还有 doSomething()handleIt(),这些名字在代码里就是灾难。

改名看起来简单,但有个坑:如果函数被多处调用,改名会影响所有调用方。我的建议是分两步走:

  1. 先添加新函数,把旧函数委托给新函数
  2. 逐个修改调用方,最后删除旧函数
// 第一步:添加新函数
function calculateTotalPrice(order) {
  return order.price * order.quantity;
}

// 旧函数委托给新函数
function calcPrice(order) {
  return calculateTotalPrice(order);
}

// 第二步:逐个修改调用方,最后删除 calcPrice

避坑指南:我曾经一次性改了20个调用方的函数名,结果漏了一个,编译报错找了一下午。后来我学乖了,用IDE的重构功能,或者至少用全局搜索确认所有调用点。

封装变量:给数据加上「看门人」

封装变量,说白了就是把直接访问的变量变成通过函数访问。为什么要这么做?因为直接暴露的数据,一旦需要增加逻辑(比如校验、缓存、日志),所有调用方都得改。

看个例子:

// 重构前:直接暴露
let defaultOwner = { firstName: "张三", lastName: "李四" };

// 重构后:封装
function getDefaultOwner() {
  return Object.assign({}, defaultOwner); // 返回副本,防止外部修改
}

function setDefaultOwner(owner) {
  // 可以加校验逻辑
  if (!owner.firstName || !owner.lastName) {
    throw new Error("owner 必须包含 firstName 和 lastName");
  }
  defaultOwner = owner;
}

重要提醒:封装变量不只是加 getter/setter。关键是要控制数据的「读写权限」和「副作用」。我见过有人封装了变量,但 getter 直接返回原始引用,外部照样能修改内部状态——这就白封装了。

封装变量的另一个好处是:将来可以轻松改成「计算属性」或「延迟加载」。比如:

function getExpensiveData() {
  if (!cachedData) {
    cachedData = computeExpensiveData();
  }
  return cachedData;
}

这种改动,调用方完全无感知。这就是封装的魅力。


这六种手法,我建议你从「提炼函数」和「提炼变量」开始练手。它们最容易上手,也最容易看到效果。等你熟练了,再尝试「内联」和「改变声明」。最后才是「封装变量」——因为它涉及数据所有权,改起来要更谨慎。

记住一句话:重构不是炫技,是为了让代码更容易理解和修改。每次重构前问自己:「这样做,下一个读代码的人会更容易理解吗?」如果答案是肯定的,那就动手吧。

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