重构
基于《重构2》一书中的思想
前部分为概念叙述,可直接跳过看例子
何为重构
- 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
- 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构
用户侧关注的行为不应改变
为何重构
- 重构改进软件设计
- 重构使软件更容易理解
- 重构帮助我们找到bug
- 重构提高变成速度
总之,重构可以帮助我们更好的掌握代码,而不是一味的完成某些事情(功能)
何时重构
三次法则:第一次做某件事的时候只管去做;第二次做类似的事情就会反感,但也能接受;第三次再做类似的事情时,你就应该重构
事不过三,三则重构
——《重构2》
- 重构使之后添加新功能更加容易
- 使代码更易懂
- 捡垃圾式重构
- 如果已经理解代码在做什么,但它做的不够好,或者冗杂,但目前有另外一个任务等待完成。这时是一个取舍。如果每次经过这段代码时,重构它,将它变得稍微好一点,积少成多,这些垃圾总会被清理完
code review时的重构- 如果一段代码不需要理解流程功能,并不需要重构它
具体的重构点
命名
通常合适的命名要比写代码更难 [滑稽]
- 命名是变成中最难的两件事之一,但是相反改名是最常用的重构手法之一
- 函数改名、变量改名、字段改名等
- 良好的命名能够让开发者一目了然,不用花费大把时间在猜谜语上
重复代码
这个不用很常见,不过多赘述
过长的函数
据我们的经验,活的最长、最好的程序,其中的函数都比较短
——《重构2》
- 函数越长越难理解,在早期的变成语言中,子程序调用需要额外的开销。但是现代编程语言几乎完全免除了进程内的函数调用开销。
- 但是小函数会使阅读上有些负担,因为需要切换频繁上下文。好在现在的开发环境都可以点击函数名快速跳转到对应函数声明处
- 当你需要写注释来说明些什么的时候,我们就要把需要说明的东西写在函数里
- 条件表达式和循环通常也是提炼的信号,例如对于一个庞大的switch语句,每个分支应该对应着一个独立的函数
过长的参数列表
- 如果发现函数的参数正在从现有的数据抽出很多数据项,不如直接传入原来的数据结果
- 如果有几项参数总是同时出现,可以传入一个对象,将参数合并为一个
- 使用类可以有效的缩短参数列表
全局变量
- 可以被修改的全局变量非常邪恶👻,因为它的全局作用域可以使它被任何地方修改
- 所以有效的拆分全局变量,变为局部变量,尽量控制其作用域,只允许当前的模块使用
可变数据
- 对于数据的修改经常导致出乎意料的结果和难以发现的bug
- 如果要更新一个数据结构,就要返回一份新的数据副本(不可变数据),使其更容易监控和推进。
......
例子——提炼函数(Extract Function)
将一个函数内部拆分成多个功能模块(函数),简洁兼并语义化
function printOwing(invoice) {
let outstanding = 0;
printBanner();
// calculate outstanding
for (const o of invoice.orders) {
outstanding += o.amount;
}
// record due date
const today = Clock.today;
invoice.dueDate = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 30
);
//print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
console.log(`due : ${invoice.dueDate.toLocaleDatestring()}`);
}
- 可以看到函数被分成了三部分:循环计算、修改属性、log
- 那么首先将log部分提炼为带两个参数的函数,同时注意函数名的语义化
- 第二步,如果局部变量(
invoice)是一个数据结构,然而代码中又有修改这个数据结构的部分,所以可以将其提炼出来为一个函数 - 第三步,移动局部变量变量(
outstanding),将初始化变量和使用变量的地方两者靠近。如果这个局部变量不止在一处使用,则将使用变量的逻辑提炼为一个函数,并且返回出变量 - 修改之后的代码如下
function printOwing(invoice) {
printBanner()
let outstanding=calculateOutstanding(invoice)
recordDueDate(invoice)
pringDetails(invoice,outstanding)
}
// ...小函数省略
-
也可将
calculateOutstanding(invoice)作为参数,即:pringDetails(invoice,calculateOutstanding(invoice))- 前提是
calculateOutstanding内用到的invoice参数不会因为代码位置的变化而变化 - 并且其他地方不会用到这个函数的返回值(
recordDueDate)
- 前提是
例子——内联函数(Inline Function)
与提炼函数恰恰相反,将一个提炼出的函数再拆开放回原函数
function getRating(driver){
return moreThanFiveLateDeliveries(driver)? 2 : 1
}
function moreThanFiveLateDeliveries(driver){
return driver.numberOfLateDeliveries > 5
}
- 如果将
moreThanFiveLateDeliveries内部的代码放回getRating中同样清晰已读,那么应该去掉这个函数。 - 但如果
moreThanFiveLateDeliveries内部代码是递归调用、多返回点、内联之后出现局部变量无法访问等复杂情况,内联函数这种重构手法并不适合。 - 上述列子这种非必要的间接性函数,duck不必
总结
从目前例子来看,思想可以总结为:为了更好的语义化,为了更好的间接性,为了更好的一目了然,
这章主要大概讲解重构的思想,当然重构还有更多种的方法,包括封装。
未完待续
参考资料
《重构2》[美]马丁.福勒(Martin Fowler)