读《重构》
介绍
花了两次读完,6 章到 11 章读起来都想拍大腿,12 章有点水。中间有很多相互矛盾的重构方法,作者想要表示的核心在于,所有的重构方法都是有其代价的,如无必要就一定不要使用。至于什么时候是必要,这就存乎一心,全看个人总结的规则和实际的情况吧。千万不能因为懒不去进行重构,看到问题要跑步前进。里面提到了领域模型,相关的书我也必须加入书单了,还有关于 Stack 错误继承 List 的问题,这里正确的实现似乎用了设计模式里面的代理。设计模式也要好好去看看了。 设计模式在这里学
6章 第一组重构
6.1 提炼函数(Extract Function)
反向重构:内联函数
能单独提取出去的操作都放到函数里1
2
3
4
5
6
7
8function printOwing(invoice){
printBanner();
let outstanding=calculateOutstanding();
//print details
console.log(`name:${invoice.customer}`);
console.log(`amoiunt:${outstanding}`);
}
重构1
2
3
4
5
6
7
8
9
10function printOwing(invoice){
printBanner();
let outstanding=calculateOutstanding();
printDetails(outstanding);
function printDetails(outstanding){
console.log(`name:${invoice.customer}`);
console.log(`amoiunt:${outstanding}`);
}
}
6.2 内联函数(Inline Function)
反向重构:提炼函数
提炼的间接性能使代码更清晰可读,但是非必要的间接性会让人不舒服。1
2
3
4
5
6function getRating(driver){
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver){
return driver.numberIdLateDeliveries >5;
}
重构1
2
3function getRating(driver){
return (driver.numberIdLateDeliveries >5) ? 2 : 1;
}
6.3提炼变量(Extract Variable)
反向重构:内联变量
表达式可能复杂而难以阅读,局部变量能帮我们把表达式分解成容易管理的模式。1
2
3return order.quantity * order.itemPrice -
Math.max(0,order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1,100);
重构
1 | const basePrice = order.quantity * order.itemPrice; |
6.4 内联变量(Inline Variable)
反向重构:提炼变量
变量是好东西,但是有时候名字并不比表达式更有表现力。1
2let basePrice=anOrder.basePrice;
return (basePrice > 1000);
重构1
return anOrder.basePrice > 1000;
6.5 改变函数声明(Change Function Declaration)
尽可能给函数取一个好名字,一旦发现好名字,尽快取修改。1
function circum(radius) { ... }//环
重构1
function circumference(radius) { ... }//周长,这里周长这个名字更准确
6.6 封装变量(Encapsulate VAriable)
重构中函数比数据更容易调整,因为函数只有调用,对数据的修改往往先从函数化封装所有对数据的访问开始。1
let defaultOwner = {firstName: "Martin",lastName: "Fowler"};
重构1
2
3let defaultOwnerData = {firstName: "Martin",lastName: "Fowler"};
export function defaultOwner() { return defalyOwnerData;}
export function setDefaultOwner (arg) {defaultOwnerData=arg};
6.7 变量改名(Rename Variable)
好的命名是整洁编程的核心。1
let a = heigjt * width;
重构1
let area = height * width;
6.8 引入参数变量(Introduce Parameter)
一组数据往往结伴同行,就是所谓数据泥团,应该代之以一个数据结构。简化参数,提升代码的一致性。1
function amountInvoiced(startDate,endData) { ... }
重构1
function amountInvoiced(aDataRange)
6.9 函数组合成类(Combine Function into Classs)
当一组函数形影不离地操作同一个数据时,就时候组建一个类了。1
2
3function base(aReading) {...}
function taxableCharge(aReading){...}
function calcuateBaseChange(aReading){...}
重构1
2
3
4
5class Reading {
base() {...}
taxableCharge(){...}
calcuateBaseCharge(){...}
}
之后7章封装,8章搬移特性,9章重新组织数据,10章简化条件逻辑,11章重构API,12章处理继承关系。读到7章就不能感同身受了,留待之后代码量更大再继续下去吧。
7章 封装
7.1 封装记录(Encapsulate Record)
1 | originization = {name: "Acme", country: "GB"} |
重构1
2
3
4
5
6
7
8
9
10class Origanization {
constructor (data){
this._name = data.name;
this._country = data.country;
}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get country() {return this._country;}
set country(arg) {this._country = arg;}
}
对于可变数据,更偏爱类对象而非记录,对象可用隐藏结构的细节,同时有助于字段的改名。
对不可变数据,记录也很好。
7.2 封装集合(Encapsulate Collection)
1 | class Person { |
重构1
2
3
4
5class Person {
get course() {return this._course.slice();}
addCourse(aCourse) { ... }
removeCourse(aCourse) { ... }
}
只对集合变量的访问进行了封装,但让取值函数返回集合本身,这使得集合的成员变量可用直接被修改,而封装它的类全然不知,无法介入。
为了避免这种情况,在类上通常加入一些修改集合的方法,比如“添加”、“移除”,这样使得对集合的修改必须经过类,当程序演化变大时,依然能轻易找出修改点。
7.3 以对象取代基本类型(Replace Primitive with Object)
1 | orders.filter(o => "hight" == o.priority |
重构1
orders.filter (o => o.prority.higherThan(new Priority("normal")))
尽量对基本类型使用类进行封装,因为随着代码的增加,这些封装的代价微乎其微。
7.4 以查询取代临时变量 (Replace Temp with Query)
1 | const basePrice = this._quantity * this._itemPrice; |
重构1
2
3
4
5
6
7
8get basePrice() {this._quantity * this._itemPrice;}
...
if (this.basePrice > 1000)
return this.basePrice * 0.95;
else
return this.basePrice * 0.98;
临时变量很好用,但是值得更进一步。将变量的计算逻辑放到函数中,有助于在提炼得到的函数于原函数之间建立清晰的边界。
7.5 提炼类(Extract Class)
反向重构:内联类
1 | class Person{ |
重构1
2
3
4
5
6
7
8class 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;}
}
常听说一个类应该是一个清晰的抽象,但是实际工作中类会不断增加东西,每次添加都觉得不必单独建立一个类。如果某些数据和函数常常成对出现,这就表示需要将它们分离出去,一个有用的测试是问自己,如果移动了这些字段和函数会发生什么。
另一个开发后期出现的信号是类的子类化,如果你发现子类化只影响类的部分特性,或者某些特性需要以一种子类化,某些特性需要另一种子类化,就意味着你需要分解原理的类。
7.6 内联类(Inline Class)
反向重构:提炼类1
2
3
4
5
6
7
8class 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;}
}
重构1
2
3
4class Person{
get officeAreaCode() {return this._officeAreaCode;}
get officeNumber() {return this._officeNumber;}
}
内联类于提炼类正好相反,如果一个类不在承担足够责任,不再有单独存在的理由(通常是因为此前的重构行为移走了这个类的责任),就以本手法将“萎缩类”塞进另一个类中。
7.7 隐藏委托关系(Hide Delegat)
反向重构:移除中间人
1 | manager = aPerson.department.manager; |
重构1
2
3
4
5manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
}
随着经验日渐丰富,你会发现,有很多值得封装的东西。
将委托关系隐藏起来,除去依赖,将来委托关系发生变化时,也只会影响服务对象,而不会直接波及所有客户端。
7.8 移除中间人(Remove Middle Man)
反向重构:隐藏委托关系1
2
3
4
5manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
}
重构1
manager = aPerson.department.manager;
封装是有代价,随着受委托的特性或功能越来越多,更多的转发函数让人烦躁,此时应该让客户直接调用受委托类。相关内容:迪米特法则。
7.9 替换算法(Subtitute Algorithm)
1 | function foundPerson(people){ |
重构1
2
3
4function foundPerson(people){
const candidate = ["Don","John","Kent"];
return people.find(p => candidate.includes(p)) || '';
}
有时候必须壮士断腕,删除整个算法,代之以简单的算法。随着对问题的理解加深,往往发现更简单的解决办法。
8章 搬移特性
在不同的上下文中间搬移元素。
8.1 搬移函数
1 | class Account { |
重构1
2
3class AccountType{
get overdraftCharge() { ... }
}
设计出高度模块化的程序,需要保证互相管理的软件要素集中到一块,并保证块与块之间的联系易于查找、直观易懂。同时对模型的设计不能一成不变,随着对代码理解的加深,我们要将这种理解反应到代码上,就得不断搬移这些元素。
8.2 搬移字段(Move Field)
1 | class Customer { |
重构
1 | class Customer { |
需要了解一些领域驱动的知识,才能更好地设计数据结构。如果发现数据结构不再适用时,就应该立刻修改。
总是一同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中。
8.3 搬移语句到函数(Move Statements into Function)
反向重构:搬移语句到调用者
1 | result.push(`<p>title: ${person.photo.title}</p>'); |
重构1
2
3
4
5
6
7
8
9result.concat(photoData(person.photo));
function photoData(aPhoto){
return [
`<p>title: ${aPhoto.title}</p>`,
`<p>location: ${aPhoto.location}</p>`,
`<p>data: ${aPhoto.date.toDateString()}</p>`
]
}
代码健康最重要的黄金法则就算“消除重复”,如果发现调用某个函数时,总有一些相同的代码也需要执行那就考虑将这段代码合并到函数里头。
8.4 搬移语句到调用者(Move Statements to Callers)
反向重构:搬移语句到函数
1 | emitPhotoData(outStream, person.photo); |
重构1
2
3
4
5
6emitPhotoData(outStream, person.photo);
outStream.write(`<p>location: ${person.photo.location}</p>\n`);
function emitPhotoData(outStream, photo){
outStream.write(`<p>title: ${photo.title}</p>\n`);
}
随着系统能力发生演化,函数的抽象边界总会悄悄发生偏移,函数边界发生偏移的一个征兆是,以往在多个地方共用的行为,如今需要在某些调用点面前表现出不同的行为。
于是我们把表现不同的行为从函数中挪出来,并搬移到器调用处。
8.5 以函数调用取代内联代码(Replace Inline Code with Function Call)
1 | let appliesToMass = false; |
重构
1 | appliesToMass = states.includes("MA"); |
善用代码将相关行为打包,并给出一个良好的命名,能避免重复。
8.6 移动语句(Slide Statements)
1 | const pricingPlan = retrievePricingPlan(); |
重构1
2
3
4const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;
让存在管理的东西一起出现,可以使代码更容易理解。如果有几行代码取用了同一个数据结构,最好让它们一起出现。
8.7 拆分循环(Split Loop)
1 | let averageAge = 0; |
重构1
2
3
4
5
6
7
8
9let 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;
拆分循环,一个循环只干一个事。这里为了后续的修改,时间复杂度的代价是值得付出的。
8.8 以管道取代循环(Replace Loop with Pipeline)
1 | const names = []; |
重构1
2
3
4const names = input
.filter(i => i.job === "program")
.map(i => i.name)
;
一些逻辑如果采用集合管道来编写,代码的可读性会更强。
8.9 移除死代码(Remove Dead Code)
1 | if (false){ |
重构1
无用的代码会带来很多思维上的负担,一旦代码不再适用,就该立即删除。
9章 重新组织数据
9.1 拆分变量(Split Variable)
1 | let temp = 2 * (height + width); |
重构1
2
3
4const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);
不要给临时变量多次赋值,保证每个变量都应该只有一个责任,并且最好命名准确。
9.2 字段改名(Rename Field)
1 | class Organization { |
重构1
2
3class Organization {
get title() { ... }
}
命名特别重要
9.3 以查询取代派生变量(Replace Derived Variable with Query)
1 | get discountedTotal() {return this._discountedTotal;} |
重构1
2get discountedTotal() {return this._baseTotal - this._discount;}
set discount(aNumber) {this._discount = aNumber;}
可变数据是软件中最大的错误源头之一,在一处修改数据,却在另一处造成难以发现的破坏,强烈建议尽量把可变数据的作用域限制在最小范围。
9.4 将引用对象改为值对象(Change Reference to Value)
反向重构:将值对象改为引用对象
1 | class Product { |
重构1
2
3
4
5class Product {
appliyDiscount(arg){
this._price = new Money (this._price.amount - arg, this._price.currency);
}
}
值对象相对来说是不可变的,可以放心把数据传给其他部分,而不用担心包装的数据被偷偷修改。
9.5 将值对象改为引用对象(Change Value to Reference)
反向重构:将引用对象改为值对象1
let customer = new Customer(customerData);
重构1
let customer = customerRepository.get(customerData.id);
如果共享的数据需要更新,就推荐适用引用对象。对于一个客观主体,只有一个代表它的对象。例如一个客户的数据,本身需要常常更新,这里使用应用对象,让数据的变化在各个函数之间共享会更好。
10章 简化条件逻辑
10.1 分解条件表达式(Decompose Conditional)
1 | if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) |
重构1
2
3
4if (summer())
charge = sumnerCharge();
else
charge = regularCharge();
将大块头代码分解未多个独立的函数,并根据其用途,为分解得到的新函数命名。
10.2 合并条件表达式(Consilidate Conditional Expression)
1 | if (anEmployee.seniority < 2) return 0; |
重构1
2
3
4
5
6if (isNotEligibleForDisability()) return 0;
function isNotEligibleForDisability(){
return ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime));
}
有时候发现一连串的检查条件最终行为一致,就应该用“逻辑与”和“逻辑或”将它们合并成一个添加表达式。
10.3 以卫语句取代嵌套添加表达式(Replace Nested Conditional with Guard Clauses)
1 | function getPayAmount () { |
重构1
2
3
4
5
6function getPayment () {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return noramlPayAmount();
}
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立即从函数中返回,这样的单独检查称为“卫语句”(guard clauses)。函数入口处判断边界条件时常用到。
10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)
1 | switch (bird.type) { |
重构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class EuropeanSwallow {
get plumage () {
return "average";
}
}
class AfricanSwallow {
get plumage () {
(bird.numberOfCoconuts > 2) ? "tired" : "average";
}
}
class NorwegianBlueParrot {
get plumage () {
return (bird.voltage > 100) ? "scorched" : "beautiful";
}
}
当出现好几个函数都有基于类型代码的 switch 语句的征兆出现,就可以针对 switch 中的每种分支逻辑创建一个类,用多态来继承各个类型特有的行为,从而去除重复的分支逻辑。
10.5 引入特例(Introduce Special Case)
1 | if (aCustomer === "unknown") customerName = "occupant"; |
重构1
2
3class UnknownCustomer {
get name() {return "occupant";}
}
一种常见的重复是一个数据结构的使用者都在在检查某个特殊的值,并且这个特殊值出现时所作的处理都相同。就可以使用特例模式。
创建一个特例元素,用以表达这个中特例的共用行为的处理。一个常见的特例处理值就算 null。
10.6 引入断言(Introduce Assertion)
1 | if (this.discountRate) |
重构1
2
3assert (this.discountRate >= 0);
if (this.discountRate)
base = base - (this.discountRate * base)
常常有只有当某个条件为真时才能正常运行的代码。这样的假设并没有在代码中明确表现。必须阅读整个算法才能看出来,有事程序员会用注释写出这种假设,但是使用断言更好。
11章 重构 API
模块和函数是软件的骨肉,而 API 则是关节。
11.1 将查询函数和修改函数分离(Separate Query from Modifier)
1 | function getTotalOutstandingAndSendBill() { |
重构1
2
3
4
5
6function totalOutstanding() {
return customer.invoices.reduce((total, sach) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(fromatBill(customer));
}
函数分为“有副作用”和“无副作用”,任何有返回值的函数都不应该有看得到的副作用。命令与查询分开(Command-Query Separation)[mf-cqs]。这个一条必须遵守的规则,它会回报很好的效果。
11.2 函数参数化(Parameterize Function)
1 | function tenPercentRaise(aPerson) { |
重构1
2
3function raise(aPerson, factor) {
aPerson.salary = aPerson.salary.mutiply(1+factor);
}
如果两个函数逻辑非常相似,只有一些字面值不同,可以将其合并成一个函数。
11.3 移除标记参数(Remove Flag Argument)
1 | function setDimension(name, value) { |
重构1
2function setHight(value) {this._height = value}
function setWidth(value) {this._width = value}
移除标记参数不仅能使代码更整洁,而且能帮助开发工具更好地发挥作用。
11.4 保持对象完整(Preseve Whole Object)
1 | const low = aRoom.daysTempRange.low; |
重构1
if (aPlan.withinRange(aRoom.daysTempRange))
如果看到代码从一个记录结构中导出几个值,然后又一起传递到另一个函数中,更好是把整个记录传给函数,在函数体内部导出所需要的值。这样能更好地应对变化。
11.5 以查询取代参数(Replace Parameter with Query)
反向重构:以参数取代查询1
2
3
4
5availbeVacation (anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
...
}
重构1
2
3availableVacation(anEmployee)P{
const grade = anEmployee.grade;
}
参数列表应该尽量避免重复,参数越少越好。
11.6 以参数取代查询(Replace Query with Parameter)
反向重构:以查询取代参数
1 | targetTemperature(aPlan) |
重构1
2
3
4
5targetTemperature(aPlan, thermost.currentTemperature)
function targetTemperature(aPlan, currentTemperature){
...
}
有时候会出现一些不愉快的引用关系,为了让函数不依赖于某个元素,就把这个元素的值以参数的形式传递给该函数。除此之外,应该尽量减短参数数目。
11.7 移除设值函数(Remove Setting Method)
1 | class Person { |
重构1
2
3class Person {
get name() { ... }
}
如果不希望再对象创建之后字段还有机会被改变,就不要为它提供设值函数。
11.8 以工厂函数取代构造函数(Replace Constructor with Factory Function)
1 | leadEngineer = new Employee(document.leadEngineer, 'E'); |
重构1
leadEngineer = createEngineer(document.leadEngineer);
使用工厂模式。
11.9 以命令取代函数(Replace Function with Command)
反向重构:以函数取代命令
1 | function socre(candidate, medicalExam, scoreingGuide) { |
重构1
2
3
4
5
6
7
8
9
10
11
12
13class Scorer {
constructot(candidate, medicalExam, socringGuide){
this._candidate = candidate;
this._medicalExam = medicalExam;
this._socringGuide = socringGuide;
}
execute() {
this._result = 0;
this._healthLevel = 0;
...
}
}
将函数封装成自己的对象,有时候也有用。命令对象提供了更大的控制灵活性和更强的表达能力。可以轻松将复杂函数拆解成多个方法,彼此之间通过字段共享状态,拆解后的方法可以分别调用,开始调用前的数据状态也可以逐步构建。
11.10 以函数取代命令(Replace Command with Function)
反向重构: 以命令取代函数
1 | class ChargeCalculator { |
重构1
2
3function charge(customer, usage){
return customer.rate * usage;
}
命令对象的强大是有代价的,大多数时候没有必要使用。
12章 处理继承关系
12.1 函数上移(Pull Up Method)
反向重构:函数下移1
2
3
4
5
6
7
8class Employee { ... }
class Salesman extends Employee{
get name() {...}
}
class Engineer extends Employee{
get name() { ... }
}
重构
1 | class Employee { |
重复代码是 bug 的温床。
12.2 字段上移(Pull Up Field)
反向重构:字段下移
1 | class Employee { ... } |
重构
1 | class Employee { |
12.3 构造函数本体上移(Pull Up Constructor Body)
1 | class Party { ... } |
重构
1 | class Party { |
各个子类中有共同行为,第一念头是将其提炼到同一个函数中,并将其上移。构造函数是一种特殊情况,因为其有特殊规则。如果重构过于复杂,就考虑使用工厂模式。
12.4 函数下移(Push Down Method)
反向重构:函数上移
如果超类中某个函数只与一个或少数几个子类相关,最好将其下移。如果超类不知道具体哪些子类需要这个函数时,使用多态取代条件表达式。
12.5 字段下移(Push Down Field)
反向重构:字段上移
12.6 以子类取代类型码(Replace Type Code with Subclasses)
反向重构:移除子类
1 | function createEmployee(name, type) { |
重构
1 | function createEmployee(name, type) { |
大多数时候,有类型码就够了,但是进一步引入子类有两个好处:
- 可以使用多态来处理相似但不同的逻辑。
- 有些字段或函数对特定的类型码才有意义,比如”销售目标“只对销售员工有效。使用类型码需要加入逻辑验证,有了子类能更明确表达这些信息。
12.7 移除子类(Remove Subclass)
反向重构:以子类取代类型码
1 | class Person { |
重构
1 | class Person { |
子类存在成本,如果用不上就别用。
12.8 提炼超类(Extract Superclass)
1 | class Department { |
重构
1 | class Party { |
如果发现两个类在做相似的事,就把它们的相似之处提炼到超类。
12.9 折叠继承体系(Collapse Hierarchy)
1 | class Employee { ... } |
重构
1 | class Employee { ... } |
有时发现一个类与其超类区别不大时,把超类子类合并。
12.10 以委托取代子类(Replace Subclass with Edlegate)
1 | class Order { |
重构
1 | class Order { |
虽然继承很好用,但继承根类之间引入了非常紧密的关系,而继承牌只能打一次。
由于导致行为不同的原因有多种,但继承只能处理一个方向上的变化。比如人可以按年龄分为两类,或者财富分为两类。继承只能使用一次,不能同时采用两种方式继承。
这两个问题用委托都能解决。
12.11 以委托取代超类(Replace Suberclass with Delegate)
1 | class List { ... } |
重构1
2
3
4
5
6class Stack {
constructor() {
this._storage = new List();
}
}
class List { ... }
在对象技术发展的早期,有一个经典误用继承的例子:让 Stack 继承 List。出发点是想复用列表类的数据存储和操作能力。但其中大部分操作对栈来说不适用。更好的办法是把列表作为栈的字段,把必要操作委派给列表。(是用代理模式吗?)