阅读《重构:改善既有代码的设计》笔记
# 阅读《重构:改善既有代码的设计》笔记
- 所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
- 本质上:重构就是在代码写好之后改进它的设计。
# 何时需要考虑重构
如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
重构步骤:
小步修改,每次修改后就运行测试。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
- 确保即将修改的代码拥有一组可靠的测试。
- 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。
- 提炼函数
- 需要检查一下,如果将这块代码提炼到自己的一个函数里,有哪些变量会离开原本的作用域。
- 考虑哪些会被修改的变量以及不会被修改的变量
- 考虑是否需要返回结果
# 重构手法
# 拆分循环
# 移动语句
# 提炼函数
将一块代码提炼到一个独立的函数中,并以这段代码的用途为这个函数命名
何时提炼:
- 被用过不止一次的代码
- 如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读 到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如 何达成其用途
- 被提炼的函数无局部变量,直接放入函数中
- 被提炼的函数有局部变量
- 被提炼代码段只是读取这些变量的值(),作为形参传入,return 值
- 被提炼代码段修改了这个变量的数据(Array,Object),作为形参传入,可选是否 return 值
eg:
// 修改前
let userInfo = {};
userInfo.name = info.name;
userInfo.age = info.age;
userInfo.sex = info.sex;
// TODO:其它对 userInfo 的操作
// 修改后
function updateInfo(info) {
let userInfo = {};
userInfo.name = info.name;
userInfo.age = info.age;
userInfo.sex = info.sex;
return userInfo;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 内联函数
某些函数,其内部代码和函数名称同样清晰易读
- 检查函数,确定它不具多态性。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 每次替换之后,执行测试。
eg:
// 修改前
function getMoney(user) {
return getDays(user) * 100;
}
function getDays(user) {
return user.days > 7 ? user.days : user.days + 1;
}
// 修改后
function getMoney(user) {
return (user.days > 7 ? user.days : user.days + 1) * 100;
}
2
3
4
5
6
7
8
9
10
11
12
# 提炼变量
表达式有可能非常复杂而难以阅读
- 确认要提炼的表达式没有副作用。
- 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
eg: 计算圆柱体表面积:2 * 2π * r * h + 2π * r^2
// 修改前
function calcCylinderArea(r, h) {
return 2 * 2π * r * h + 2π * r^2
}
// 修改后
function calcCylinderArea(r, h) {
const rectArea = 2π * r^2 * h
const circleArea = 2π * r
return 2 * circleArea + rectArea
}
2
3
4
5
6
7
8
9
10
11
# 内联变量
在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西
- 检查确认变量赋值语句的右侧表达式没有副作用
- 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 测试。
- 重复前面两步,逐一替换其他所有使用该变量的地方。
- 删除该变量的声明点和赋值语句。
- 测试。
eg:
// 修改前
let days = user.days;
return days > 7;
// 修改后
return user.days > 7;
2
3
4
5
6
# 改变函数声明
一个好名字能让人一眼看出函数的用途,而不必查看其实现代码
- 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
- 修改函数声明,使其成为你期望的状态。
- 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
- 测试。
eg:
// 修改前
function getInfo(url) {
return fetch({ url }).then((res) => res.json());
}
// 修改后
function getUserInfoById(url) {
return fetch({ url }).then((res) => res.json());
}
2
3
4
5
6
7
8
9
# 封装变量
如果想要搬移一处被广泛使用的数据,最好的办法往往是先以函数形式封装所有对该数据的访问
这样,就能把“重新组织数据”的困难任务转化 为“重新组织函数”这个相对简单的任务。
- 创建封装函数,在其中访问和更新变量值。
- 执行静态检查。
- 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,
- 执行测试。
- 限制变量的可见性。
eg:
// 修改前
let name = 'coderly'
let age = 18
let sex = '男'
function getSex() {
return sex
}
// 修改后
let user = {
name: 'coderly',
age: 18,
sex = '男'
}
function getSex() {
return user.sex
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 变量改名
变量可以很好地解释一段程序在干什么 —— 如果变量名起得好的话
- 如果变量被广泛使用,考虑运用『封装变量』将其封装起来。
- 找出所有使用该变量的代码,逐一修改。
- 测试
eg:
// 修改前
let url = "https://coderly.cn";
function updateUrl(newUrl) {
url = url;
}
// 修改后
let myBlogUrl = "https://coderly.cn";
function updateMyBlogUrl(newBlogUrl) {
myBlogUrl = newBlogUrl;
}
2
3
4
5
6
7
8
9
10
11
# 引入参数对象
将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得明晰
使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。
- 如果暂时还没有一个合适的数据结构,就创建一个
- 测试。
- 使用『改变函数声明』给原来的函数新增一个参数,类型是新建的数据结构。
- 测试。
- 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。测试。
eg:
// 修改前
const station = {
name: "coderly",
readings: [
{ temp: 47, time: "2021-12-05 09:10" },
{ temp: 53, time: "2021-12-05 09:20" },
{ temp: 58, time: "2021-12-05 09:30" },
{ temp: 53, time: "2021-12-05 09:40" },
{ temp: 51, time: "2021-12-05 09:50" },
],
};
// 找到超出指定范围的温度读数
function readingsOutsideRange(station, min, max) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
// 调用
readingsOutsideRange(station, min, max);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 修改后
const station = {
name: "coderly",
readings: [
{ temp: 47, time: "2021-12-05 09:10" },
{ temp: 53, time: "2021-12-05 09:20" },
{ temp: 58, time: "2021-12-05 09:30" },
{ temp: 53, time: "2021-12-05 09:40" },
{ temp: 51, time: "2021-12-05 09:50" },
],
};
function readingsOutsideRange(station, range) {
return station.readings.filter(
(r) => r.temp < range.min || r.temp > range.max
);
}
class NumberRange {
constructor(min, max) {
this._data = { min: min, max: max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
}
// 使用
let range = new NumberRange(min, max);
readingsOutsideRange(station, range);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 函数组合成类
如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),就可以认为,是时候组建一个类了
类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。
使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。
- 运用『封装记录』对多个函数共用的数据记录加以封装。
- 对于使用该记录结构的每个函数,运用『搬移函数』将其移入新类。
- 用以处理该数据记录的逻辑可以用『提炼函数』提炼出来,并移入新类。
# 函数组合成变换
先用源 数据创建一个类,再把相关的计算逻辑搬移到类中
如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
- 测试。
- 针对其他相关的计算逻辑,重复上述步骤。
# 拆分阶段
一段代码在同时处理两件不同的事,就把它拆分成各自独立的模块
- 将第二阶段的代码提炼成独立的函数。
- 测试。
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
- 测试。
- 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用 到,就将其移入中转数据结构。每次搬移之后都要执行测试。