很多人认为函数的作用是复用代码或减少代码长度,但在《重构:改善既有代码的设计》中,Martin Fowler 提出了一个更重要的观点:函数的意义是表达意图,而不是实现。
当阅读代码时,如果需要花时间理解一段逻辑在做什么,就应该提炼函数,让调用层只表达业务意图,而实现细节隐藏在函数内部。
本文从"重复代码(Duplicated Code)"这一常见坏味道出发,总结三种常见的重构方式:提炼函数、移动语句、函数上移,并通过简单示例说明如何让代码从"读逻辑"变成"读意图"。
一、如何消除重复代码
1. 完全重复:提炼函数(Extract Function)
最简单的重复代码可以直接 提炼函数。
scss
// bad
if (
user.age >= 18 &&
user.emailVerified &&
!user.suspended
) {
grantAccess(user)
}
scss
// good
if (canUserAccessSystem(user)) {
grantAccess(user)
}
const canUserAccessSystem = (user) =>
user.age >= 18 &&
user.emailVerified &&
!user.suspended
函数命名表达意图,调用处只需要理解做什么。
2. 相似但不相同:移动语句 → 再提炼
有些代码看起来类似,但顺序不同或夹杂其他逻辑。
处理方式:
- 移动语句
将相关的数据和逻辑移动到一起。 - 重组结构
- 提炼函数
核心思想:
先让代码结构变得一致,再消除重复。
👍移动语句 的目标是改善代码结构,将 数据与相关逻辑聚集在一起。
移动时需要注意:
- 变量依赖顺序
- 副作用顺序
3. 子类中的重复:函数上移(Pull Up Method)
当多个子类实现了相同的方法时,可以将方法提升到父类。
scala
class Engineer extends Employee {
int getAnnualSalary() {
return monthlySalary * 12;
}
}
class Manager extends Employee {
int getAnnualSalary() {
return monthlySalary * 12;
}
}
重构后:
csharp
class Employee {
int getAnnualSalary() {
return monthlySalary * 12;
}
}
父类更适合定义共同行为。
二、何时应该提炼函数?
很多人认为:
- 函数是为了复用
- 函数是为了缩短代码
其实都不是。
作者给了一个很实用的判断方式:
如果你需要花时间浏览一段代码才能弄清它在干什么。
就应该提炼函数。
当然,也可能是你还没读懂代码😁
换句话说:
读代码应该读意图,而不是读逻辑。
三、提炼函数的价值
好的函数命名可以让代码变成一种"叙事结构"。
例如:
scss
processOrder()
validateOrder()
calculatePrice()
applyDiscount()
sendEmail()
调用层只表达 业务流程,实现细节隐藏在函数内部。
提炼函数最直接的收益是:
降低阅读成本✨
四、提炼函数步骤
提炼函数通常可以按下面步骤进行:
- 根据代码意图命名函数
- 检查变量依赖
外部变量通过参数传入 - 编译代码
- 用函数调用替换原代码
- 运行测试验证
- 查找其他重复代码并替换
如果一开始想不到好名字也没关系。
先提炼函数,在重构过程中名字往往会逐渐清晰。
如果最后发现不合适,可以再 内联回去。
这不是无用功。
五、实践技巧
1. 嵌套在源函数
在被提炼代码的源函数内部定义新函数,可以减少参数数量。
javascript
function processOrder(order) {
function calculatePrice() {
...
}
}
注意:
如果有需要,可以提炼到外部
2. 注意变量赋值
如果被提炼代码对变量进行了赋值:新函数应该 返回新的值。
如果需要返回多个变量:通常说明这段代码 暂时不适合提炼函数。
六、总结
处理重复代码通常有三种方式:
| 场景 | 重构方式 |
|---|---|
| 完全重复 | 提炼函数 |
| 相似但顺序不同 | 移动语句 → 再提炼 |
| 子类重复实现 | 函数上移 |
而提炼函数最核心的原则只有一句话:
代码应该表达意图,而不是实现。 👊