重构-改善既有代码的设计(二)
重构名录-第一组重构
提炼函数(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(`amount:${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(`amount:${outstanding}`);
}
}动机
“将意图和实现分开”:如果你需要花时间浏览一段代码才能弄清楚它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途。
做法
- 创造一个新的函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)
- 待提炼的代码从源函数复制到新建的目标函数中
- 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的便令。若是,以参数的形式将它们传递给新函数。
- 所有变量都处理完之后,编译。
- 在源函数中,将被提炼代码替换为对目标函数的调用。
- 测试
- 查看其它代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。
内联函数(Inline Function)
示例
1
2
3
4
5
6
7function getRating(driver){
return moreThanFiveLateDeliveries(dirver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver){
return driver.numberOfLateDeliveries > 5;
}重构为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function getRating(driver){
return (driver.numberOfLateDeliveries >5) ? 2 : 1;
}
``
* 动机
1. 某些函数,其内部代码和函数名称同样清晰易读,你就应该去掉这个函数,直接使用其中的代码。
2. 手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以喜欢的方式重新提炼出小函数
3. 间接层有其价值,但不是所有间接层都有价值。通过内联手法,可以找出那些有用的间接层,同时将无用的间接层去除。
* 做法
1. 检查函数,确定它不具多态性。
2. 找出这个函数的所有调用点。
3. 将这个函数的所有调用点都替换成函数本体。
4. 每次替换之后,执行测试。
5. 删除该函数的定义。
#### 提炼函数(Extract Variable)
* 示例
```js
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1,100)重构为
1
2
3
4const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePirce * 0.01, 100);
return basePrice - quantityDiscount + shipping;动机
- 表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。
- 这样的变量在调试时也很方便,他们给调试器和打印语句提供了便利的抓手。
做法
- 确认要提炼的表达式没有副作用。
- 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
- 测试。
内联变量(Inline Variable)
示例
1
2let basePrice = anOther.basePrice;
return (basePrice > 1000);重构为
1
return anOther.basePrice > 1000;
动机
在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。做法
- 检查确认变量赋值语句的右侧表达式没有副作用。
- 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 测试。
- 重复前面两步,逐一替换其他所有使用该变量的地方。
- 删除该变量的声明点和赋值语句。
- 测试。
改变函数声明(Change Function Declaration)
示例
1
function circum(radius){...}
重构为
1
function circumference(radius){...}
动机
- 一个好的函数名字能让我一眼看出函数的用途,而不必查看其实现代码。
- 函数的参数列表阐述了函数如何与外部世界共处。
- 修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件。
做法
简单的做法
- 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
- 修改函数声明,使其成为你期望的状态。
- 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
- 测试
迁移式做法
- 如果有必要的话,先对函数体内部加以重构,使后面的提炼不走易于开展。
- 使用提炼函数方法将函数体提炼成一个新函数。
- 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
- 测试。
- 对旧函数使用内联函数。
- 如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字。
- 测试。
封装变量(Encapsulate Variable)
示例
1
let defaultOwner = {firstName:"Martion",lastName:"Fowler"};
重构为
1
2
3let defaultOwnerData = {firstName:"Martion",lastName:"Fowler"};
export function defaultOwner() { return defaultOwnerData; }
export function setDefaultOwner(args) { defaultOwnerData = args; }动机
- 如果想要搬移一处被广泛使用的数据,最好的方法往往是先以函数形式封装所有对该数据的访问。
- 封装能提供一个清晰的观测点。可以由此监控数据的变化和使用情况;还可以轻松地添加数据被修改时的验证或后续逻辑。
做法
- 创建封装函数,在其中访问和更新变量值。
- 执行静态检查。
- 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
- 限制变量的可见性。
- 测试。
- 如果变量的值是一个记录,考虑使用封装记录。
变量改名(Rename Variable)
示例
1
let a = height * width
重构为
1
let area = height * width
动机
- 变量可以很好地解释一段程序在干什么–如果变量名起得好的话。
- 使用范围越广,名字的好坏就越重要。
做法
- 如果变量被广泛使用,考虑运用封装变量将其封装起来。
- 找出所有使用该变量的代码,逐一修改。
- 测试。
引入参数对象(Introduce Parameter Object)
示例
1
2
3function amountInvoiced(startDate, endDate){...}
function amountReceived(startDate, endDate){...}
function amountOcerdue(startDate, endDate){...}重构为
1
2
3function amountInvoiced(aDateRange){...}
function amountReceived(aDateRange){...}
function amountOcerdue(aDateRange){...}动机
- 将数据组织成结构是一件有价值的事,因为这让数据项之间的关系变得清晰。
- 经过重构之后,所有使用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一致性。
- 会催生代码中更深层次的改变。一旦识别出新的数据结构,就可以重组程序的行为来使用这些结构。
做法
- 如果暂时还没有一个合适的数据结构,就创建一个。
- 测试。
- 使用改变函数声明方法给原来的函数新增一个参数,类型是新建的数据结构。
- 测试。
- 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
- 用新数据结构中每项元素,逐一取代参数列表中与之对应1的参数项,然后删除原来的参数。测试。
函数组合成类(Combine Functions into Class)
示例
1
2
3function base(aReading){...}
function taxableCharge(aReading){...}
function calculateBaseCharge(aReading){...}重构为
1
2
3
4
5class Reading{
base(){...}
taxableCharge(){...}
calculateBaseCharge(){...}
}动机
- 如果发现一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确地给这些函数提供一个功用的环境,在对象内部调用这些函数可以少传很多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他地方。
- 除了可以把已有的函数组织起来,我们还可以去发现其他的计算逻辑,将它们也重构到新的类当中。
- 使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会自动与核心数据保持一致。
做法
- 运用封装记录方法对多个函数共用的数据记录加以封装。
- 对于使用该记录结构的每个函数,运用搬移函数方法将其移入新类。
- 用以处理该数据记录的逻辑可以用提炼函数方法提炼出来,并移入新类。
函数组合成变换(Combine Functions into Transform)
示例
1
2function base(aReading){...}
function taxableCharge(aReading){...}重构为
1
2
3
4
5
6function enrichReading(argReading){
const aReading = _.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
return aReading;
}动机
- 把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
- 数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。
- 函数组合成类:先用源数据创建一个类,再把相关的计算逻辑搬移到类中。
- 区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储到新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。
做法
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
- 测试
- 针对其他相关的计算逻辑,重复上述步骤。
拆分阶段(Split Phase)
示例
1
2
3const 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
13const 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];
}动机
- 每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块,因为这样到了需要修改的时候,我就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。
- 最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段。可能你有一段逻辑处理,其输入数据的格式不符合计算逻辑的要求,所以你得先对输入数据做一番调整,使其便于处理。也可能是你把数据处理洛安吉分成顺序执行的多个步骤,每个步骤负责的任务全然不同。
做法
- 将第二阶段的代码提炼成独立的函数。
- 测试。
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
- 测试。
- 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移后都要执行测试。
- 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构。
重构-改善既有代码的设计(二)
http://blog.chcaty.cn/2021/04/27/chong-gou-gai-shan-ji-you-dai-ma-de-she-ji-er/