第5章:介绍重构目录(Introducing the Refactoring Catalog)
一、导读
1.1 本章学习目标
第5章是《重构》的工具箱指南。Martin Fowler 在本章中介绍了重构目录的使用方法——每个重构手法的描述格式、如何查阅目录、如何选择合适的手法。掌握重构目录,你就拥有了一套系统化的代码改善工具。
本章的核心学习目标包括:
- 理解重构手法的描述格式:名称、动机、做法、示例、变体
- 掌握查阅重构目录的方法:如何快速找到适合当前场景的手法
- 学会组合使用多种重构手法:单个手法往往不够,组合使用效果更佳
- 理解重构手法的适用场景和限制:不是所有手法都适用于所有情况
- 建立重构手法的直觉:看到代码问题就知道该用什么手法
1.2 为什么本章如此重要
重构不是随意的代码修改,而是有章可循的系统化改善。重构目录就是这种系统化的体现——它将多年的实践经验提炼为可复用、可传授的手法。就像医学中的手术步骤一样,每个重构手法都有明确的步骤、预期效果和注意事项。
掌握重构目录的价值在于:当你面对一段"坏代码"时,你不需要从零开始思考如何改善——你可以查阅目录,找到已经验证过的手法,按照步骤执行。这极大地提高了重构的效率和安全性。
1.3 核心问题引导
重构手法的描述格式是什么?每个部分有什么作用?
如何选择合适的重构手法?
重构手法之间有什么关联?如何组合使用?
重构手法的"做法"为什么如此详细?能否跳过某些步骤?
如何判断一个重构手法是否适用于当前场景?
二、核心概念详解
2.1 重构手法的描述格式
Fowler 为每个重构手法定义了统一的描述格式:
- 名称(Name):手法的简洁描述,如"Extract Function"
- 动机(Motivation):为什么要做这个重构?什么场景下使用?
- 做法(Mechanics):详细的步骤说明,确保每一步都是安全的
- 示例(Example):重构前后的代码对比
- 变体(Variations):手法在不同编程语言或场景下的变化
## Extract Function(提炼函数)
### 动机
当你看到一段代码可以放在一起被一个名称注释时,就把它提取到一个独立的函数中。
### 做法
1. 创建一个新函数,以它的意图命名
2. 将需要提取的代码复制到新函数中
3. 检查是否有局部变量需要作为参数传递
4. 在原位置用函数调用替换提取的代码
5. 运行测试
### 示例
// 重构前
function printOwing(invoice) {
printBanner();
// calculate outstanding
let outstanding = 0;
for (const o of invoice.orders) {
outstanding += o.amount;
}
printDetails(outstanding);
}
// 重构后
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(outstanding);
}
function calculateOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {
outstanding += o.amount;
}
return outstanding;
}2.2 如何查阅重构目录
Fowler 的重构目录按照坏味道组织——当你发现某种坏味道时,可以查阅对应的重构手法:
| 坏味道 | 推荐的重构手法 |
|---|---|
| 神秘命名 | Change Function Declaration, Rename Variable |
| 重复代码 | Extract Function, Slide Statements |
| 过长函数 | Extract Function, Replace Temp with Query |
| 过长参数列 | Introduce Parameter Object, Remove Flag Argument |
| 发散式变化 | Extract Class, Move Function |
| 霰弹式修改 | Move Function, Move Field, Inline Class |
2.3 重构手法的组合使用
在实际重构中,很少只使用一个手法。通常是多个手法的组合:
// 初始代码:多种坏味道混合
function process(data) {
let result = '';
for (let i = 0; i < data.length; i++) {
if (data[i].type === 'A') {
result += data[i].value * 1.1;
} else if (data[i].type === 'B') {
result += data[i].value * 0.9;
}
}
return result;
}
// 组合使用多个重构手法
// 1. Extract Function:提取计算逻辑
// 2. Replace Conditional with Polymorphism:用多态替代条件
// 3. Move Function:将逻辑移到合适的类
// 步骤1:提取函数
function process(data) {
return data.reduce((sum, item) => sum + calculateItem(item), 0);
}
function calculateItem(item) {
if (item.type === 'A') return item.value * 1.1;
if (item.type === 'B') return item.value * 0.9;
return item.value;
}
// 步骤2:用多态替代条件
class ItemProcessor {
calculate(item) { return item.value; }
}
class TypeAProcessor extends ItemProcessor {
calculate(item) { return item.value * 1.1; }
}
class TypeBProcessor extends ItemProcessor {
calculate(item) { return item.value * 0.9; }
}
function process(data) {
return data.reduce((sum, item) => {
const processor = getProcessor(item.type);
return sum + processor.calculate(item);
}, 0);
}2.4 重构手法的分类
Fowler 将重构手法分为几大类:
2.4.1 封装类
- Extract Function / Inline Function
- Rename Variable / Extract Variable
- Encapsulate Record / Encapsulate Collection
2.4.2 搬移类
- Move Function / Move Field
- Move Statements into Function / Move Statements to Callers
2.4.3 数据组织类
- Split Variable / Remove Assignments to Parameters
- Replace Derived Data with Updater
- Change Reference to Value / Change Value to Reference
2.4.4 条件逻辑类
- Decompose Conditional
- Replace Conditional with Polymorphism
- Introduce Special Case
- Replace Conditional with State/Strategy
2.4.5 API 类
- Separate Query from Modifier
- Parameterize Function
- Remove Setting Method
2.4.6 继承类
- Pull Up Field / Pull Up Method
- Push Down Method / Push Down Field
- Replace Inheritance with Delegation
三、实战案例
3.1 案例:使用重构目录改善代码
以下是一个实际的重构过程,展示了如何查阅目录并组合使用多种手法。
// 初始代码:包含多种坏味道
class OrderService {
processOrder(order) {
let total = 0;
for (const item of order.items) {
if (item.type === 'ELECTRONICS') {
total += item.price * 1.13;
} else if (item.type === 'BOOK') {
total += item.price;
} else if (item.type === 'FOOD') {
total += item.price * 1.05;
}
}
if (order.customer.isVIP) {
total *= 0.9;
}
if (total > 100) {
total -= 10;
}
return { total, status: 'COMPLETED' };
}
}
// 重构过程:
// 1. 识别坏味道:过长函数、条件逻辑复杂
// 2. 查阅目录:Decompose Conditional, Extract Function
// 3. 逐步重构
class OrderService {
processOrder(order) {
const subtotal = this.calculateSubtotal(order.items);
const discount = this.calculateDiscount(order.customer, subtotal);
const total = subtotal - discount;
return { total, status: 'COMPLETED' };
}
calculateSubtotal(items) {
return items.reduce((sum, item) => sum + this.calculateItemPrice(item), 0);
}
calculateItemPrice(item) {
const taxRate = this.getTaxRate(item.type);
return item.price * taxRate;
}
getTaxRate(type) {
const rates = {
'ELECTRONICS': 1.13,
'BOOK': 1.0,
'FOOD': 1.05
};
return rates[type] || 1.0;
}
calculateDiscount(customer, subtotal) {
let discount = 0;
if (customer.isVIP) discount += subtotal * 0.1;
if (subtotal > 100) discount += 10;
return discount;
}
}四、本章小结
第5章介绍了重构目录的使用方法:
重构手法有统一的描述格式:名称、动机、做法、示例、变体
目录按坏味道组织:发现问题就能找到对应的手法
手法可以组合使用:多个手法配合解决复杂问题
手法分为几大类:封装、搬移、数据组织、条件逻辑、API、继承
做法步骤必须遵循:每一步都是经过验证的安全步骤
本章的核心思想是:重构是一门手艺,而不是一门艺术。它有章可循,有据可查,可以通过学习和练习来掌握。
五、思考与练习
思考题
重构手法与编程模式有什么关系?
为什么重构手法的"做法"要如此详细?
如何判断一个重构手法是否适用于当前的编程语言?
重构目录是否需要随编程语言的发展而更新?
编程练习
选择 3 个不同的重构手法,分别在实际代码中练习。
尝试组合使用 3 个以上的重构手法来改善一段复杂代码。
为你团队常用的编程语言编写一份"重构手法速查表"。