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()}`);
}
你看,原来的函数有四个职责:打印横幅、计算金额、记录日期、打印详情。现在每个函数只做一件事,名字就是它的文档。
我的习惯:给新函数取名时,多用动词+名词的结构,比如 calculateOutstanding、validateInput。别用 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(),读起来反而更费劲。后来我学乖了:提炼之前先问自己一句——"这个提炼真的让代码更好读了吗?"
嗯,这四种手法就先聊到这儿。它们是重构的"基本功",就像练武要先扎马步一样。别嫌简单,真正用好它们,你的代码质量会上一个台阶。