代码重构
在不改变代码外在行为的前提下,对代码进行修改,以改进程序的内部结构
- 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
- 重构前,先检查自己是否有一套可靠的测试代码。这些测试必须有自我检验能力。
- 重构技术就是以微小的步伐修改程序。 如果你犯下错误,很容易便可发现它。
重构类型
- 小型重构:在类内部完成,可以借助IDE自动化来进行
- 中型重构:涉及到类之间,要充分做好测试
- 大型重构:对整个系统的架构进行重构优化,需要有计划地进行,时间不短
重构度量
对于中小型重构,可以观察代码健康度相关的指标变化来度量重构的价值:比如代码的圈复杂度、平均函数行数、类行数等
而对于大型重构,则可以通过工程效率上的指标变化来可视化重构的收益
- 需求平均缺陷率:测试中发现的缺陷数除以需求开发时长总和
- 迭代内故事一次性通过率:开发完成后一次性通过验收和测试的需求数除以需求总数
- 版本发布成功率:发布版本时一次性通过验收的次数除以总发布次数
- 端到端交付周期:特性从规划到最终发布的时间间隔的平均值
- 故事平均开发周期:故事停留在开发阶段的时间总和(包括打回后)的平均值
- 缺陷平均解决周期:缺陷从提交到最终修复的时间间隔的平均值
- 故事平均测试周期:故事停留在测试阶段的时间总和(包括打回后)的平均值
- 缺陷/故事的流转次数:需求在进入开发阶段之后到关闭之前转给不同开发的次数
- 技术债务:代码中存在的不符合最佳实践或设计原则的部分
重构原则
为何重构
- 改进软件的设计
- 使代码更容易理解
- 提高编程速度
何时重构
- 预备性重构:添加新功能的时候
- 帮助理解的重构:为了理解系统或者代码所做的工作
- 捡垃圾式重构:偶然发现一处坏代码,重构它
- 修复错误的时候
- 代码审查的时候
何时不该重构
- 不会被用到的代码
- 重构的代价比重写的代价还高的代码
如何保证重构的正确性
测试是保证代码正确性的强有力保证
- 自动化
- 测试不通过真的会失败
- 频繁运行测试
- 注意边界条件
- 使用测试来重现bug
代码的坏味道
- 奇怪的命名
- 重复代码
- 过长的函数
- 过长的参数列表
- 全局数据
- 可变数据
- 发散式变化
- 一个修改会影响到许多地方
- 霰弹式修改
- 一个变化需要修改许多地方
- 过度依赖外部模块
- 类中重复的数据
- 基本类型偏执
- 总觉得基本类型效率更高,不愿使用对象
- 大量重复的switch/if
- 复杂的循环语句
- 冗余的元素
- 一个简单的函数、一个简单的操作
- 过度设计的通用性
- 过度考虑了对象/函数的用途
- 临时字段
- 过长的对象调用
- 没有必要的中间对象
- 两个模块耦合过紧
- 考虑将它们移动到新模块
- 过大的类
- 过度相似的类
- 纯数据类
- 数据和行为没有在一起
- 继承父类,但不提供父类的接口
重构列表
函数/变量
- 提炼函数
根据代码意图进行拆分函数,如果发现一段代码需要阅读一会才能知道是干嘛的,那就提炼它
function printOwing(invoice) { printBanner(); let outstanding = calculateOutstanding(); //print details console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`);}
↓
function printOwing(invoice) { printBanner(); let outstanding = calculateOutstanding(); printDetails(outstanding); function printDetails(outstanding) { console.log(`name: ${invoice.customer}`); console.log(`amount: ${outstanding}`); }}
- 内联函数
提炼函数的反向操作
如果函数的代码跟函数名称一样拥有可读性,那么可以直接内联它
- 提炼变量
给一些表达式起个有意义的名称,有助于阅读、调试
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;
- 内联变量
上述的反向重构
有些表达式本身就已经很有语义,没必要引入变量再来说明
- 改变函数签名
注意函数签名的上下文,不同的上下文通用性程度不一样
直接修改
迁移式
- 暴露新旧两个接口,将旧接口设置为废弃
封装变量
对于访问域过大的数据,使用函数进行封装,这样在重构、监控上更加容易
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
↓
let defaultOwnerData = {firstName: "Martin", lastName: "Fowler"};export function defaultOwner() {return defaultOwnerData;}export function setDefaultOwner(arg) {defaultOwnerData = arg;}
- 变量改名
好的命名是整洁代码的核心
- 引入参数对象
让数据项自己的关系变得清晰,并且缩短参数列表
function amountInvoiced(startDate, endDate) {...} function amountReceived(startDate, endDate) {...} function amountOverdue(startDate, endDate) {...}
↓
function amountInvoiced(aDateRange) {...} function amountReceived(aDateRange) {...} function amountOverdue(aDateRange) {...}
- 函数组合成类
发现行为与数据之间的联系,发现其他的计算逻辑
function base(aReading) {...}function taxableCharge(aReading) {...} function calculateBaseCharge(aReading) {...}
↓
class Reading { base() {...} taxableCharge() {...} calculateBaseCharge() {...}}
- 合并函数
对于多个操作相同的数据,并且逻辑可以集中的函数,可以将它们合并成同一个函数
function base(aReading) {...}function taxableCharge(aReading) {...}
↓
function enrichReading(argReading) { const aReading = _.cloneDeep(argReading); aReading.baseCharge = base(aReading); aReading.taxableCharge = taxableCharge(aReading); return aReading;}
- 拆分阶段
一段代码做了多件事,将它拆分为多个函数
封装
- 封装记录
封装能更好地应对变化
organization = {name: "Acme Gooseberries", country: "GB"};
↓
class Organization {...}
- 封装集合
对集合成员变量进行封装,返回其一个副本,避免其被修改带来的诸多问题
class Person { get courses() {return this._courses;} set courses(aList) {this._courses = aList;}}
↓
class Person { get courses() {return this._courses.slice();} addCourse(aCourse) { ... } removeCourse(aCourse) { ... }}
- 以对象取代基本类型
一开始使用基本类型能很好地表示,但随着代码演进,这些数据可能会产生一些行为,此时最好将其封装为对象
orders.filter(o => "high" === o.priority || "rush" === o.priority);
↓
orders.filter(o => o.priority.higherThan(new Priority("normal")))
- 以查询取代临时变量
使用函数封装临时变量的计算,对于可读性、可复用性有提升
const basePrice = this._quantity * this._itemPrice; if (basePrice > 1000) return basePrice * 0.95; else return basePrice * 0.98;
↓
get basePrice() {this._quantity * this._itemPrice;}...if (this.basePrice > 1000) return this.basePrice * 0.95;else return this.basePrice * 0.98;
- 提炼类
随着代码演进,类不断成长,会变得越加复杂,需要拆分它
class Person { get officeAreaCode() {return this._officeAreaCode;} get officeNumber() {return this._officeNumber;}}
↓
class Person { get officeAreaCode() {return this._telephoneNumber.areaCode;} get officeNumber() {return this._telephoneNumber.number;}}class TelephoneNumber { get areaCode() {return this._areaCode;} get number() {return this._number;}}
- 内联类
上述的反向操作,由于类职责的改变,或者两个类合并在一起会更加简单
- 隐藏委托关系
封装意味着模块间相互了解的程度应该尽可能小,一旦发生变化,影响也会较小
manager = aPerson.department.manager;
↓
manager = aPerson.manager; class Person { get manager() {return this.department.manager;}}
- 移除中间人
上述的反向操作,对于一些没必要的委托,可以直接让其跟真实对象打交道,避免中间层对象成为一个纯粹的转发对象
- 替换算法
不改变行为的前提下,将比较差的算法替换成比较好的算法
function foundPerson(people) { for(let i = 0; i < people.length; i++) { if (people[i] === "Don") { return "Don"; } if (people[i] === "John") { return "John"; } if (people[i] === "Kent") { return "Kent"; } } return "";}
↓
function foundPerson(people) { const candidates = ["Don", "John", "Kent"]; return people.find(p => candidates.includes(p)) || '';}
搬移特性
- 搬移函数
对于某函数,如果它频繁使用了其他上下文的元素,那么就考虑将它搬移到那个上下文里
class Account { get overdraftCharge() {...}}
↓
class AccountType { get overdraftCharge() {...}}
- 搬移字段
对于早期设计不良的数据结构,使用此方法改造它
class Customer { get plan() {return this._plan;} get discountRate() {return this._discountRate;}}
↓
class Customer { get plan() {return this._plan;} get discountRate() {return this.plan.discountRate;}}
- 搬移语句到函数
使用这个方法将分散的逻辑聚合到函数里面,方便理解修改
result.push(`<p>title: ${person.photo.title}</p>`); result.concat(photoData(person.photo));function photoData(aPhoto) { return [ `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>`, ];}
↓
result.concat(photoData(person.photo));function photoData(aPhoto) { return [ `<p>title: ${aPhoto.title}</p>`, `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>`, ];}
- 搬移语句到调用者
上述的反向操作
对于代码演进,函数某些代码职责发生变化,将它们移除出去
- 以函数调用取代内联代码
一些函数的函数名就拥有足够的表达能力
let appliesToMass = false; for(const s of states) { if (s === "MA") appliesToMass = true;}
↓
appliesToMass = states.includes("MA");
- 移动语句
让存在关联的东西一起出现,可以使代码更容易理解
const pricingPlan = retrievePricingPlan(); const order = retreiveOrder();let charge;const chargePerUnit = pricingPlan.unit;
↓
const pricingPlan = retrievePricingPlan(); const chargePerUnit = pricingPlan.unit; const order = retreiveOrder();let charge;
- 拆分循环
对一个循环做了多件事的代码,拆分它,使各段代码职责更加明确
虽然这样可能会对性能造成一些损失
let averageAge = 0;let totalSalary = 0;for (const p of people) { averageAge += p.age; totalSalary += p.salary;}averageAge = averageAge / people.length;
↓
let totalSalary = 0;for (const p of people) { totalSalary += p.salary;}let averageAge = 0;for (const p of people) { averageAge += p.age;}averageAge = averageAge / people.length;
- 以管代取代循环
一些逻辑如果采用管道编写,可读性会更强
const names = [];for (const i of input) { if (i.job === "programmer") names.push(i.name);}
↓
const names = input .filter(i => i.job === "programmer") .map(i => i.name);
- 移除死代码
移除那些永远不会允许的代码
重新组织数据
- 拆分变量
如果一个变量被用于多种用途,很明显违反了单一职责原则,这样的代码会造成理解上的困难
let temp = 2 * (height + width); console.log(temp);temp = height * width; console.log(temp);
↓
const perimeter = 2 * (height + width); console.log(perimeter);const area = height * width; console.log(area);
- 字段改名
对于命名不够良好的字段进行改名
- 以查询取代派生变量
使用查询封装变量是消除可变数据的第一步
get discountedTotal() {return this._discountedTotal;} set discount(aNumber) { const old = this._discount; this._discount = aNumber; this._discountedTotal += old - aNumber;}
↓
get discountedTotal() {return this._baseTotal - this._discount;} set discount(aNumber) {this._discount = aNumber;}
- 将引用对象改为值对象
如果非一定需要引用对象,使用值对象不可变的特性能避免很多问题
- 将值对象改为引用对象
如果一个对象需要在多个地方做更新,值对象就不适合了,需要改为引用
简化条件逻辑
- 分解条件表达式
使用函数封装条件逻辑,提升代码的可理解性
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) charge = quantity * plan.summerRate;else charge = quantity * plan.regularRate + plan.regularServiceCharge;
↓
if (summer()) charge = summerCharge(); else charge = regularCharge();
- 合并条件表达式
一些条件的返回值都相等,就将它们封装到同一个函数逻辑里面
if (anEmployee.seniority < 2) return 0;if (anEmployee.monthsDisabled > 12) return 0;if (anEmployee.isPartTime) return 0;
↓
if (isNotEligibleForDisability()) return 0; function isNotEligibleForDisability() { return ((anEmployee.seniority < 2) || (anEmployee.monthsDisabled > 12) || (anEmployee.isPartTime));}
- 以卫语句取代嵌套条件表达式
有时候单一出口原则,似乎不是那么重要
function getPayAmount() { let result; if (isDead) result = deadAmount(); else { if (isSeparated) result = separatedAmount(); else { if (isRetired) result = retiredAmount(); else result = normalPayAmount(); } } return result;}
↓
function getPayAmount() { if (isDead) return deadAmount(); if (isSeparated) return separatedAmount(); if (isRetired) return retiredAmount(); return normalPayAmount();}
- 以多态取代条件表达式
如果发现一些行为适合用多态取代,试试这样重构它
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 EuropeanSwallow { get plumage() { return "average"; }class AfricanSwallow { get plumage() { return (this.numberOfCoconuts > 2) ? "tired" : "average"; }class NorwegianBlueParrot { get plumage() { return (this.voltage > 100) ? "scorched" : "beautiful";}
- 引入特例
所谓特例,就是满足这个类的行为,但却表达了特例的含义
if (aCustomer === "unknown") customerName = "occupant";
↓
class UnknownCustomer { get name() {return "occupant";}
- 引入断言
断言提供了一种对系统当前状态的假设,对调试以及阅读很有帮助
if (this.discountRate) base = base - (this.discountRate * base);
↓
assert(this.discountRate>= 0); if (this.discountRate) base = base - (this.discountRate * base);
重构API
- 查询函数和修改函数分离
对于无副作用的函数,有助于测试
function getTotalOutstandingAndSendBill() { const result = customer.invoices.reduce((total, each) => each.amount + total, 0); sendBill(); return result;}
↓
function totalOutstanding() { return customer.invoices.reduce((total, each) => each.amount + total, 0);}function sendBill() { emailGateway.send(formatBill(customer));}
- 函数参数化
本质还是消除重复,将函数名字中的参数提取到参数列表中
function tenPercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.1);}function fivePercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.05);}
↓
function raise(aPerson, factor) { aPerson.salary = aPerson.salary.multiply(1 + factor);}
- 移除标记参数
标记参数的存在会增加理解接口调用的难度
function setDimension(name, value) { if (name === "height") { this._height = value; return; } if (name === "width") { this._width = value; return; }}
↓
function setHeight(value) {this._height = value;} function setWidth (value) {this._width = value;}
- 保持对象完整
传递整个对象能更好地应对未来的变化
const low = aRoom.daysTempRange.low; const high = aRoom.daysTempRange.high; if (aPlan.withinRange(low, high))
↓
if (aPlan.withinRange(aRoom.daysTempRange))
- 以查询取代参数
参数列表尽量避免重复,参数列表越短越容易理解
availableVacation(anEmployee, anEmployee.grade); function availableVacation(anEmployee, grade) {}
↓
availableVacation(anEmployee)function availableVacation(anEmployee) {}
- 以参数取代查询
上述操作的反向重构,如果不想函数依赖某个元素,那就使用这个方式
- 移除设值函数
取消设值函数,代表着数据不应该被修改的意图
class Person { get name() {...} set name(aString) {...}}
↓
class Person { get name() {...}}
- 以工厂函数取代构造函数
构造函数使用起来较不灵活,尝试把创建对象的职责交给工厂
leadEngineer = new Employee(document.leadEngineer, 'E');
↓
leadEngineer = createEngineer(document.leadEngineer);
- 以命名取代函数
命令对象大都服务于单一的函数,命令相交于过程性代码,拥有了大部分面向对象的能力
function score(candidate, medicalExam, scoringGuide) { let result = 0; let healthLevel = 0; // long body code}
↓
class Scorer { constructor(candidate, medicalExam, scoringGuide) { this._candidate = candidate; this._medicalExam = medicalExam; this._scoringGuide = scoringGuide; } execute() { this._result = 0; this._healthLevel = 0; // long body code }}
- 以函数取代命令
上述的反向重构,在不是很复杂的情况下,直接使用函数完成任务即可
处理继承关系
- 函数上移
本质上还是为了避免重复,重复代码是滋生bug的温床
class Employee {...}class Salesman extends Employee { get name() {...}}class Engineer extends Employee { get name() {...}}
↓
class Employee { get name() {...}}class Salesman extends Employee {...} class Engineer extends Employee {...}
- 字段上移
同上,函数换成字段
- 构造函数本体上移
将子类里的共同行为提取到父类
class Party {...}class Employee extends Party { constructor(name, id, monthlyCost) { super(); this._id = id; this._name = name; this._monthlyCost = monthlyCost; }}
↓
class Party { constructor(name){ this._name = name; }}class Employee extends Party { constructor(name, id, monthlyCost) { super(name); this._id = id; this._monthlyCost = monthlyCost; }}
- 函数下移
函数上移的反向重构,如果超类的某个函数只与部分子类有关,那就需要将函数下移
- 字段下移
字段上移的反向重构,动机同上
- 以子类取代类型码
使用多态来替代逻辑判断
function createEmployee(name, type) { return new Employee(name, type);}
↓
function createEmployee(name, type) { switch (type) { case "engineer": return new Engineer(name); case "salesman": return new Salesman(name); case "manager": return new Manager (name);}
- 移除子类
随着代码演进,子类压根就不需要了
class Person { get genderCode() {return "X";}}class Male extends Person { get genderCode() {return "M";}}class Female extends Person { get genderCode() {return "F";}}
↓
class Person { get genderCode() {return this._genderCode;}}
- 提炼超类
如果两个类再做相似的事,利用继承机制将它们的相似之处进行提炼
class Department { get totalAnnualCost() {...} get name() {...} get headCount() {...}}class Employee { get annualCost() {...} get name() {...} get id() {...}}
↓
class Party { get name() {...} get annualCost() {...}}class Department extends Party { get annualCost() {...} get headCount() {...}}class Employee extends Party { get annualCost() {...} get id() {...}}
- 折叠继承体系
随着继承体系演化,一个类与其超类已经没有多大区别
class Employee {...}class Salesman extends Employee {...}
↓
class Employee {...}
- 以委托取代子类
继承会给子类带来极大的耦合,父类的任何修改都会影响到子类,使用委托就是一种组合关系,在任何情况下,组合应该优先于继承
class Order { get daysToShip() { return this._warehouse.daysToShip; }}class PriorityOrder extends Order { get daysToShip() { return this._priorityPlan.daysToShip; }}
↓
class Order { get daysToShip() { return (this._priorityDelegate) ? this._priorityDelegate.daysToShip : this._warehouse.daysToShip; }}class PriorityOrderDelegate { get daysToShip() { return this._priorityPlan.daysToShip }}
- 以委托取代超类
如果父类的一些接口不适合让子类暴露,那么这个类应该就通过组合的方式复用
class List {...}class Stack extends List {...}
↓
class Stack { constructor() { this._storage = new List(); }}class List {...}
速查表
坏味道(英文) | 坏味道(中文) | 页码 | 常用重构 |
---|---|---|---|
Alternative Classes with Different Interfaces | 异曲同工的类 | 83 | 改变函数声明(124),搬移函数(198),提炼超类(375) |
Comments | 注释 | 84 | 提炼函数(106),改变函数声明(124),引入断言(302) |
Data Class | 纯数据类 | 83 | 封装记录(162),移除设值函数(331),搬移函数(198),提炼函数(106),拆分阶段(154) |
Data Clumps | 数据泥团 | 78 | 提炼类(182),引入参数对象(140),保持对象完整(319) |
Divergent Change | 发散式变化 | 76 | 拆分阶段(154),搬移函数(198),提炼函数(106),提炼类(182) |
Duplicated Code | 重复代码 | 72 | 提炼函数(106),移动语句(223),函数上移(350) |
Feature Envy | 依恋情结 | 77 | 搬移函数(198),提炼函数(106) |
Global Data | 全局数据 | 74 | 封装变量(132) |
Insider Trading | 内幕交易 | 82 | 搬移函数(198),搬移字段(207),隐藏委托关系(189),以委托取代子类(381),以委托取代超类(399) |
Large Class | 过大的类 | 82 | 提炼类(182),提炼超类(375),以子类取代类型码(362) |
Lazy Element | 冗赘的元素 | 80 | 内联函数(115),内联类(186),折叠继承体系(380) |
Long Function | 过长函数 | 73 | 提炼函数(106),以查询取代临时变量(178),引入参数对象(140),保持对象完整(319),以命令取代函数(337),分解条件表达式(260),以多态取代条件表达式(272),拆分循环(227) |
Long Parameter List | 过长参数列 | 74 | 以查询取代参数(324),保持对象完整(319),引入参数对象(140),移除标记参数(314),函数组合成类(144) |
Loops | 循环语句 | 79 | 以管道取代循环(231) |
Message Chains | 过长的消息链 | 81 | 隐藏委托关系(189),提炼函数(106),搬移函数(198) |
Middle Man | 中间人 | 81 | 移除中间人(192),内联函数(115),以委托取代超类(399),以委托取代子类(381) |
Mutable Data | 可变数据 | 75 | 封装变量(132),拆分变量(240),移动语句(223),提炼函数(106),将查询函数和修改函数分离(306),移除设值函数(331),以查询取代派生变量(248),函数组合成类(144),函数组合成变换(149),将引用对象改为值对象(252) |
Mysterious Name | 神秘命名 | 72 | 改变函数声明(124),变量改名(137),字段改名(244) |
Primitive Obsession | 基本类型偏执 | 78 | 以对象取代基本类型(174),以子类取代类型码(362),以多态取代条件表达式(272),提炼类(182),引入参数对象(140) |
Refused Bequest | 被拒绝的遗赠 | 83 | 函数下移(359),字段下移(361),以委托取代子类(381),以委托取代超类(399) |
Repeated Switches | 重复的switch | 79 | 以多态取代条件表达式(272) |
Shotgun Surgery | 霰弹式修改 | 76 | 搬移函数(198),搬移字段(207),函数组合成类(144),函数组合成变换(149),拆分阶段(154),内联函数(115),内联类(186) |
Speculative Generality | 夸夸其谈通用性 | 80 | 折叠继承体系(380),内联函数(115),内联类(186),改变函数声明(124),移除死代码(237) |
Temporary Field | 临时字段 | 80 | 提炼类(182),搬移函数(198),引入特例(289) |