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%的重构场景。