重构目录
这是 Martin Fowler《Refactoring》(第 2 版)中的一份精选重构目录。每个重构都包含动机、操作步骤和示例。
“重构由它的操作步骤来定义,也就是你执行这个变化时遵循的精确过程。” — Martin Fowler
如何使用这个目录
- 识别异味:先用代码异味目录定位问题
- 找到对应重构:在这里查找匹配的手法
- 按步骤执行:一步一步来
- 每步都测试:确保行为不变
黄金法则:如果某一步超过 10 分钟,就把它拆得更小。
最常见的重构
Extract Method
何时使用:长函数、重复代码、需要为概念命名
动机:把一段代码抽成一个名字能说明意图的方法。
步骤:
- 创建一个新方法,名字应描述“做什么”而不是“怎么做”
- 把代码片段复制到新方法里
- 检查片段里用了哪些局部变量
- 把局部变量作为参数传入,或在方法内声明
- 正确处理返回值
- 用新方法调用替换原始片段
- 测试
前:
function printOwing(invoice) {
let outstanding = 0;
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
// Calculate outstanding
for (const order of invoice.orders) {
outstanding += order.amount;
}
// Print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}后:
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
}
function calculateOutstanding(invoice) {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function printDetails(invoice, outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}Inline Method
何时使用:方法体和名字一样清楚,或者只是多余转发
动机:当方法没有额外价值时,去掉不必要的间接层。
步骤:
- 确认这个方法不是多态方法
- 找到所有调用点
- 用方法体替换每个调用
- 每替换一次就测试一次
- 删除方法定义
前:
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
何时使用:变量名没有比表达式更清楚
动机:移除没必要的间接层。
步骤:
- 检查右侧表达式没有副作用
- 如果变量不是不可变的,先改成不可变并测试
- 找到第一次引用并替换成表达式
- 测试
- 对所有引用重复
- 删除变量声明和赋值
- 测试
Rename Variable
何时使用:名字没有清楚表达用途
动机:好名字是清晰代码的基础。
步骤:
- 如果变量使用范围很广,先考虑封装
- 找到所有引用
- 逐个修改引用
- 测试
提示:
- 使用能体现意图的名字
- 避免缩写
- 使用领域术语
// Bad
const d = 30;
const x = users.filter(u => u.a);
// Good
const daysSinceLastLogin = 30;
const activeUsers = users.filter(user => user.isActive);Change Function Declaration
何时使用:函数名不能清楚说明用途,参数也需要变化
动机:好函数名能让代码自解释。
步骤(简单情况):
- 删除不需要的参数
- 修改函数名
- 添加需要的参数
- 测试
步骤(迁移式,适用于复杂改动):
- 如果要删除参数,确保它没有被使用
- 创建一个新函数,声明符合新设计
- 让旧函数调用新函数
- 测试
- 让调用方逐步改用新函数
- 每改一个调用点就测试
- 删除旧函数
前:
function circum(radius) {
return 2 * Math.PI * radius;
}后:
function circumference(radius) {
return 2 * Math.PI * radius;
}Encapsulate Variable
何时使用:多个地方直接访问某个数据
动机:为数据访问提供明确入口。
步骤:
- 创建 getter 和 setter
- 找到所有引用
- 用 getter 替换读取
- 用 setter 替换写入
- 每次改动后测试
- 降低变量可见性
前:
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
// Used in many places
spaceship.owner = defaultOwner;后:
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
function defaultOwner() { return defaultOwnerData; }
function setDefaultOwner(arg) { defaultOwnerData = arg; }
spaceship.owner = defaultOwner();Introduce Parameter Object
何时使用:一组参数经常一起出现
动机:把自然属于一起的数据打包起来。
步骤:
- 为这组参数创建一个新类 / 结构
- 测试
- 用 Change Function Declaration 引入新对象
- 测试
- 逐个移除参数,改用对象字段
- 每次移除后测试
前:
function amountInvoiced(startDate, endDate) { ... }
function amountReceived(startDate, endDate) { ... }
function amountOverdue(startDate, endDate) { ... }后:
class DateRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
}
function amountInvoiced(dateRange) { ... }
function amountReceived(dateRange) { ... }
function amountOverdue(dateRange) { ... }Combine Functions into Class
何时使用:多个函数操作同一份数据
动机:把函数和它操作的数据放到一起。
步骤:
- 对公共数据先做 Encapsulate Record
- 把每个函数移动到类里
- 每移动一次就测试
- 用类字段替代数据参数
前:
function base(reading) { ... }
function taxableCharge(reading) { ... }
function calculateBaseCharge(reading) { ... }后:
class Reading {
constructor(data) { this._data = data; }
get base() { ... }
get taxableCharge() { ... }
get calculateBaseCharge() { ... }
}Split Phase
何时使用:代码在处理两件不同的事
动机:把代码拆成边界清晰的两个阶段。
步骤:
- 为第二阶段创建新函数
- 测试
- 在两个阶段之间引入中间数据结构
- 测试
- 把第一阶段提取成独立函数
- 测试
前:
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
return basePrice - discount + shippingCost;
}后:
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
return { basePrice, quantity, discount };
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}移动特性
Move Method
何时使用:方法更依赖另一个类的数据而不是自己类的数据
动机:把函数放到它最依赖的数据所在的类里。
步骤:
- 检查方法使用到的所有程序元素
- 确认方法不是多态的
- 把方法复制到目标类
- 调整上下文
- 让原方法委托给目标方法
- 测试
- 视情况删除原方法
Move Field
何时使用:字段更多被另一个类使用
动机:让数据和使用它的函数靠在一起。
步骤:
- 如果还没有,先封装字段
- 测试
- 在目标类中创建字段
- 把引用改成使用目标字段
- 测试
- 删除原字段
Move Statements into Function
何时使用:相同代码总是跟着函数调用一起出现
动机:把重复语句移入函数,去掉重复。
步骤:
- 如果还没有,先把重复代码提取成函数
- 把语句移入函数
- 测试
- 如果调用方不再需要独立语句,就删除它们
Move Statements to Callers
何时使用:不同调用方需要不同的行为
动机:当行为需要差异化时,把它从函数里移出去。
步骤:
- 先对要移动的代码做 Extract Method
- 再对原函数做 Inline Method
- 删除被内联后的调用
- 把提取出的代码移到各个调用方
- 测试
数据组织
Replace Primitive with Object
何时使用:数据项需要的不只是简单值
动机:把数据和行为封装在一起。
步骤:
- 先做 Encapsulate Variable
- 创建一个简单值对象
- 修改 setter,让它创建新实例
- 修改 getter,让它返回值
- 测试
- 给新类增加更丰富的行为
前:
class Order {
constructor(data) {
this.priority = data.priority; // string: "high", "rush", etc.
}
}
// Usage
if (order.priority === "high" || order.priority === "rush") { ... }后:
class Priority {
constructor(value) {
if (!Priority.legalValues().includes(value))
throw new Error(`Invalid priority: ${value}`);
this._value = value;
}
static legalValues() { return ['low', 'normal', 'high', 'rush']; }
get value() { return this._value; }
higherThan(other) {
return Priority.legalValues().indexOf(this._value) >
Priority.legalValues().indexOf(other._value);
}
}
// Usage
if (order.priority.higherThan(new Priority("normal"))) { ... }Replace Temp with Query
何时使用:临时变量保存的是一个表达式结果
动机:把表达式提取成函数,让代码更清晰。
步骤:
- 确保变量只被赋值一次
- 把赋值右侧提取成一个方法
- 用方法调用替换临时变量引用
- 测试
- 删除临时变量声明和赋值
前:
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}后:
get basePrice() {
return this._quantity * this._itemPrice;
}
// In the method
if (this.basePrice > 1000) {
return this.basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}简化条件逻辑
Decompose Conditional
何时使用:复杂条件语句
动机:把条件和分支动作分别提取出来,让意图更清楚。
步骤:
- 对条件做 Extract Method
- 对 then 分支做 Extract Method
- 对 else 分支也做 Extract Method(如果有)
前:
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}后:
if (isSummer(aDate, plan)) {
charge = summerCharge(quantity, plan);
} else {
charge = regularCharge(quantity, plan);
}
function isSummer(date, plan) {
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
}
function summerCharge(quantity, plan) {
return quantity * plan.summerRate;
}
function regularCharge(quantity, plan) {
return quantity * plan.regularRate + plan.regularServiceCharge;
}Consolidate Conditional Expression
何时使用:多个条件最后都返回同一个结果
动机:让人一眼看出这些条件其实是一道检查。
步骤:
- 确认条件没有副作用
- 用 and / or 合并条件
- 视情况对合并后的条件做 Extract Method
前:
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;后:
if (isNotEligibleForDisability(employee)) return 0;
function isNotEligibleForDisability(employee) {
return employee.seniority < 2 ||
employee.monthsDisabled > 12 ||
employee.isPartTime;
}Replace Nested Conditional with Guard Clauses
何时使用:深层嵌套条件让流程难以追踪
动机:用 guard clause 提前返回,让正常流程更清楚。
步骤:
- 找出特殊情况
- 用提前返回的 guard clause 替换它们
- 每改一步就测试
前:
function payAmount(employee) {
let result;
if (employee.isSeparated) {
result = { amount: 0, reasonCode: "SEP" };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonCode: "RET" };
} else {
result = calculateNormalPay(employee);
}
}
return result;
}后:
function payAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
return calculateNormalPay(employee);
}Replace Conditional with Polymorphism
何时使用:按类型分支的 switch / 条件逻辑
动机:让对象自己处理自己的行为。
步骤:
- 创建类层次(如果还没有)
- 用 Factory Function 创建对象
- 把条件逻辑移到超类方法里
- 给每种情况创建子类方法
- 删除原条件
前:
function plumages(birds) {
return birds.map(b => plumage(b));
}
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
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 NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
}
function createBird(data) {
switch (data.type) {
case 'EuropeanSwallow': return new EuropeanSwallow(data);
case 'AfricanSwallow': return new AfricanSwallow(data);
case 'NorwegianBlueParrot': return new NorwegianBlueParrot(data);
default: return new Bird(data);
}
}Introduce Special Case (Null Object)
何时使用:重复出现 null 检查
动机:返回一个特殊对象来处理特殊情况。
步骤:
- 创建一个具备预期接口的特殊情况类
- 增加 isSpecialCase 检查
- 引入工厂方法
- 用特殊对象替换 null 检查
- 测试
前:
const customer = site.customer;
// ... many places checking
if (customer === "unknown") {
customerName = "occupant";
} else {
customerName = customer.name;
}后:
class UnknownCustomer {
get name() { return "occupant"; }
get billingPlan() { return registry.defaultPlan; }
}
// Factory method
function customer(site) {
return site.customer === "unknown"
? new UnknownCustomer()
: site.customer;
}
// Usage - no null checks needed
const customerName = customer.name;重构 API
Separate Query from Modifier
何时使用:函数既返回值又有副作用
动机:明确哪些操作会产生副作用。
步骤:
- 创建新的查询函数
- 复制原函数的返回逻辑
- 修改原函数,让它只负责副作用
- 替换依赖返回值的调用点
- 测试
前:
function alertForMiscreant(people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return "Don";
}
if (p === "John") {
setOffAlarms();
return "John";
}
}
return "";
}后:
function findMiscreant(people) {
for (const p of people) {
if (p === "Don") return "Don";
if (p === "John") return "John";
}
return "";
}
function alertForMiscreant(people) {
if (findMiscreant(people) !== "") setOffAlarms();
}Parameterize Function
何时使用:有多个做类似事情但数值不同的函数
动机:通过参数化减少重复。
步骤:
- 选择一个函数
- 为变化的字面值增加参数
- 修改函数体使用该参数
- 测试
- 让调用方改用参数化版本
- 删除不再使用的旧函数
前:
function tenPercentRaise(person) {
person.salary = person.salary * 1.10;
}
function fivePercentRaise(person) {
person.salary = person.salary * 1.05;
}后:
function raise(person, factor) {
person.salary = person.salary * (1 + factor);
}
// Usage
raise(person, 0.10);
raise(person, 0.05);Remove Flag Argument
何时使用:布尔参数改变函数行为
动机:通过拆分成明确函数,让行为更清楚。
步骤:
- 针对不同旗标值创建显式函数
- 替换每个调用点
- 每次改动后测试
- 删除原函数
前:
function bookConcert(customer, isPremium) {
if (isPremium) {
// premium booking logic
} else {
// regular booking logic
}
}
bookConcert(customer, true);
bookConcert(customer, false);后:
function bookPremiumConcert(customer) {
// premium booking logic
}
function bookRegularConcert(customer) {
// regular booking logic
}
bookPremiumConcert(customer);
bookRegularConcert(customer);继承相关
Pull Up Method
何时使用:多个子类里有相同方法
动机:去掉类层次中的重复。
步骤:
- 检查方法是否完全相同
- 确认签名一致
- 在超类中新建方法
- 从一个子类复制实现
- 删除一个子类中的方法并测试
- 删除其他子类中的方法并测试
Push Down Method
何时使用:某个行为只适用于部分子类
动机:把方法放到真正使用它的地方。
步骤:
- 把方法复制到需要它的子类
- 从超类删除方法
- 测试
- 删除不需要该方法的子类副本
- 测试
Replace Subclass with Delegate
何时使用:继承用得不对,需要更灵活
动机:在合适的地方更偏向组合而不是继承。
步骤:
- 创建一个空的委托类
- 在宿主类中加一个持有委托的字段
- 在宿主类构造委托
- 把功能迁移到委托类
- 每次迁移后测试
- 用委托替代继承
Extract Class
何时使用:大类里有多个职责
动机:拆分类以维持单一职责。
步骤:
- 决定如何拆分职责
- 创建新类
- 把字段从原类移到新类
- 测试
- 把方法从原类移到新类
- 每次移动后测试
- 重新检查并命名两个类
- 决定如何暴露新类
前:
class Person {
get name() { return this._name; }
set name(arg) { this._name = arg; }
get officeAreaCode() { return this._officeAreaCode; }
set officeAreaCode(arg) { this._officeAreaCode = arg; }
get officeNumber() { return this._officeNumber; }
set officeNumber(arg) { this._officeNumber = arg; }
get telephoneNumber() {
return `(${this._officeAreaCode}) ${this._officeNumber}`;
}
}后:
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get telephoneNumber() { return this._telephoneNumber.toString(); }
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
toString() { return `(${this._areaCode}) ${this._number}`; }
}快速参考:异味到重构
| 代码异味 | 主要重构 | 备选方案 |
|---|---|---|
| 长函数 | Extract Method | Replace Temp with Query |
| 重复代码 | Extract Method | Pull Up Method |
| 大类 | Extract Class | Extract Subclass |
| 长参数列表 | Introduce Parameter Object | Preserve Whole Object |
| Feature Envy | Move Method | Extract Method + Move |
| 数据泥团 | Extract Class | Introduce Parameter Object |
| 基础类型沉迷 | Replace Primitive with Object | Replace Type Code |
| switch 语句 | Replace Conditional with Polymorphism | Replace Type Code |
| 临时字段 | Extract Class | Introduce Null Object |
| 消息链 | Hide Delegate | Extract Method |
| 中间人 | Remove Middle Man | Inline Method |
| 发散式变化 | Extract Class | Split Phase |
| 散弹式修改 | Move Method | Inline Class |
| 死代码 | Remove Dead Code | - |
| 臆想泛化 | Collapse Hierarchy | Inline Class |
延伸阅读
- Fowler, M. (2018). Refactoring: Improving the Design of Existing Code (第 2 版)
- 在线目录:https://refactoring.com/catalog/

