重构 改善既有代码的设计 - Martin Fowler

《重构》第 2 版使用 JavaScript 作为示例语言,详细介绍了重构的原则、方法和具体手法。全书包含 60 多种重构手法,帮助开发者在不改变代码外部行为的前提下,持续改进代码的内部结构。
关于作者
Martin Fowler 是软件工程领域的权威专家:
- ThoughtWorks 首席科学家:全球知名软件咨询公司
- 敏捷开发先驱:与 Kent Beck 等人共同推动敏捷运动
- UML 创始人之一:参与统一建模语言的创建
- 技术作家:著有《企业应用架构模式》《分析模式》等经典著作
- NoSQL 运动推动者:最早提出 NoSQL 概念
经典摘录
重构的定义:在不改变代码外部行为的前提下,改善其内部结构。
两次法则:第一次做某件事时只管去做;第二次再做类似的事情时可能会反感,但还是照做;第三次再做时,就应该重构了。
任何傻瓜都能写出计算机可以理解的代码。优秀的程序员写出的是人类能理解的代码。
当你需要添加新功能但代码结构不适合时,先重构代码使结构清晰,再添加新功能。
测试是重构的安全网。没有可靠的测试,就不要重构。
核心重构手法
1. 提炼函数 (Extract Function)
// 重构前
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 = new Date();
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 = new Date();
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()}`);
}
2. 以对象取代基本类型 (Replace Primitive with Object)
// 重构前
const order = {
customer: 'acme',
priority: 'high', // 字符串,容易拼写错误
};
function isHighPriority(order) {
return order.priority === 'high'; // 硬编码
}
// 重构后
class Priority {
constructor(value) {
this._value = value;
}
get value() {
return this._value;
}
get isHigh() {
return this._value === 'high';
}
static createHigh() {
return new Priority('high');
}
static createNormal() {
return new Priority('normal');
}
static createLow() {
return new Priority('low');
}
}
const order = {
customer: 'acme',
priority: Priority.createHigh(),
};
function isHighPriority(order) {
return order.priority.isHigh; // 使用对象方法
}
3. 保持接口完整 (Preserve Whole Object)
// 重构前:传递多个参数
function bookConcert(customer, theater, date, time, seats) {
// ...
}
// 重构后:传递对象
function bookConcert(bookingRequest) {
const { customer, theater, date, time, seats } = bookingRequest;
// ...
}
4. 以多态取代条件表达式 (Replace Conditional with Polymorphism)
// 重构前
class Bird {
get plumage() {
if (this._type === 'EuropeanSwallow') {
return 'average';
} else if (this._type === 'AfricanSwallow') {
return this._numberOfCoconuts > 2 ? 'tired' : 'average';
} else if (this._type === 'NorwegianParr') {
return 'scared';
}
return 'unknown';
}
}
// 重构后
class Bird {
get plumage() {
return 'unknown';
}
}
class EuropeanSwallow extends Bird {
get plumage() {
return 'average';
}
}
class AfricanSwallow extends Bird {
get plumage() {
return this._numberOfCoconuts > 2 ? 'tired' : 'average';
}
}
class NorwegianParr extends Bird {
get plumage() {
return 'scared';
}
}
重构与测试
// 重构的前提:可靠的测试
describe('Invoice', () => {
it('calculates outstanding correctly', () => {
const invoice = {
customer: 'acme',
orders: [
{ amount: 100 },
{ amount: 200 },
],
};
const result = calculateOutstanding(invoice);
expect(result).toBe(300);
});
it('handles empty orders', () => {
const invoice = {
customer: 'acme',
orders: [],
};
const result = calculateOutstanding(invoice);
expect(result).toBe(0);
});
});
读书心得
《重构》第 2 版最大的改进是使用 JavaScript 作为示例语言,让前端开发者更容易理解和实践。Fowler 在书中强调:
- 小步重构:每次只做小的改动,逐步积累
- 测试保障:重构前确保有可靠的测试
- 持续重构:重构不是一次性活动,而是日常习惯
书中 60 多种重构手法涵盖了从简单的"提炼函数"到复杂的"以多态取代条件表达式",每一种都有详细的步骤说明和代码示例。
对我影响最深的是两次法则:第一次做时只管去做,第二次可能还会忍受,第三次就必须重构了。这个简单的原则帮助我在实际工作中及时发现和消除代码异味。