《重构:改善既有代码的设计》读书笔记

阅读《重构:改善既有代码的设计》第二版,学到很多思想和技能,特此记录

书中演示代码主要以 JavaScript 为主

何时重构

书中提到一些代码的坏味道

神秘命名(Mysterious Name)

命名是编程中最难的两件事之一。正因为如此,修改命名可能是最常用的重构方法。如果你发现改名很难,那就说明代码设计有问题。当我们不能给一个模块,一个对象,一个函数,甚至一个变量找到合适名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象。

重复代码(Duplicated Code)

同一类的两个函数含有相同的表达式,就应该提炼。

过长函数(Long Function)

活得最长,最好的程序,其中函数一般都很短。如果你觉得需要写注释,大部分情况就代表这个东西需要写进一个独立的函数里面,然后根据用途来命名比较好。条件表达式和循环往往也是提炼函数的信号。

过长参数列表(Long Parameter List)

使用类可以有效的缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。

全局数据(Global Data)

全局数据仍然是最刺鼻的坏味道之一。它的问题是,全局数据在任何地方都可以被修改。所以正确的做法是将全局数据封装起来,用函数将其包起来,这样就知道那些地方修改了它。有少量的全局数据或者无妨,但数量越多,处理难度就会指数上升。(良药与毒药的区别在于剂量)

可变数据(Mutable Data)

核心是缩小作用域。可以通过封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易被监控。

发散式变化(Divergent Change)

发散式变化指某个模块因为不同的原因在不同的方向上发生变化。每次只关心一个上下文。找到引起发散式变化的原因,将它拆分出来。

霰弹式修改(Shotgun Surgery)

在每次修改的时候,应该只修改一处,而不是到处的修改。因为一个需求,需要修改 3 处代码,那么这就需要思考,这 3 处代码是否应该抽离出来。一个常用的策略就是使用内联(inline)重构代码把本不该分散的逻辑拽回一处。

依恋情结(Feature Envy)

模块化,力求代码分出区域,最大化区域内部交互,最小化区域间交互。如果两个模块交互频繁,它们应该合并在一起。

数据泥团(Data Clumps)

如果在多个类中,出现了很多相同项的数据,你需要想想是否要通过将数据提炼成类,来抽离出一个独立对象。建议新建类而非简单的结构体。

基本类型偏执(Primitive Obsession)

很多程序员不愿意创建对自己的问题域有用的基本类型,如钱,坐标,范围等。比如有程序员用字符串来表示电话号码,实际上你应该抽象出来一个电话号码对象。

重复的 switch(Repeated Switches)

尽量使用多态而非 switch。

循环语句(Loops)

我们应该用管道操作(如 filter 和 map)来替代循环,这样能更快的看清被处理的元素和处理他们的动作。

冗赘的元素(Lasy Element)

能简单的代码,尽量简单。未来变复杂的时候,再去考虑它。

夸夸其谈的通用性(Speculative Generality)

同上,能简单的代码,尽量简单。通用性?过早的优化是万恶之源

临时字段(Temporary Field)

临时字段指内部某个字段仅为某种特定情况而设。临时的字段不应该存在。你需要给他们搬个新家,把所有和临时变量相关的代码搬至那里。

过长的消息链(Message Chains)

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象,这就是消息链。消息链意味着客户端会耦合消息链的查找过程。应该将查找过程独立出一个函数。

中间人(Middle Man)

委托函数过多时,减少委托,移除中间人,让调用者直接访问目标类进行操作。

内幕交易(Insider Trading)

减少模块之间频繁的数据交换,并把这种交换放到明面上。

过大的类(Large Class)

当一个类代码行数太多或者功能职责太多的时候,拆掉它。两种拆分方法:提取新类,当大类的部分行为可以分解为一个单独的组件,则可以使用提取类的方式拆分。提取子类,当大类的部分行为可以以不同的方式实现或在极少数情况下使用,则可以使用提取子类方式拆分。

异曲同工的类(Alternative Classes with Different Interfaces)

两个类有着相同的功能,但方法名称不同。重命名方法,并去除掉不必要的重复代码。

纯数据类(Data Class)

纯数据类常常意味着行为被放在了错误的地方。处理数据的行为应该从客户端移至纯数据类中。

被拒绝的遗赠(Refused Bequest)

如果子类复用了父类的实现,就应该支持父类的接口。

注释(Comments)

注释是提示你,这个地方该重构啦。如果你觉得需要写注释的时候,请先重构,试着让所有注释都变得多余。

重构手法

提炼函数(Extract Function)

将独立逻辑的一段代码,提炼为一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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}`);
}
}

提炼函数的动机:将意图与实现分开

内联函数(Inline Function)

将函数中的代码直接写在调用处

1
2
3
4
5
6
7
8
9
10
11
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)

分解复杂表达式,用局部变量分解表达式

1
2
3
4
5
6
7
8
9
10
11
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)

消除表现力不好的变量

1
2
let basePrice = anOrder.basePrice;
return basePrice > 1000;
1
return anOrder.basePrice > 1000;

改变函数声明(Change Function Declaration)

函数改名,函数名应表达其作用

1
function circum(radius) {...}
1
function circumference(radius) {...}

封装变量(Encapsulate Variable)

避免直接修改全局数据,以函数形式封装对该数据的访问

1
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
1
2
3
4
5
6
7
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
export function defaultOwner() {
return defaultOwnerData;
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}

变量改名(Rename Variable)

将变量名改为易解释的名称

1
let a = height * width;
1
let area = height * width;

引入参数对象(Introduce Parameter Object)

将一组参数,合并为一个对象进行传参

1
2
3
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
1
2
3
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}

函数组合成类(Combine Functions into Class)

用面向对象的的方式,组合一组函数

1
2
3
function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}
1
2
3
4
5
class Reading {
base() {...}
taxableCharge() {...}
calculateBaseCharge() {...}
}

函数组合成变换(Combine Functions into Transform)

将一组函数组合为一个函数,返回结果组合成对象

1
2
function base(aReading) {...}
function taxableCharge(aReading) {...}
1
2
3
4
5
6
function enrichReading(argReading) {
const aReading = _.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
return aReading;
}

拆分阶段(Split Phase)

将一段代码,根据处理的事不同,拆分为多个函数

1
2
3
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
1
2
3
4
5
6
7
8
9
10
11
12
13
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(aString) {
const values = aString.split(/\s+/);
return {
productID: values[0].split("-")[1],
quantity: parseInt(values[1]),
};
}
function price(order, priceList) {
return order.quantity * priceList[order.productID];
}