重构的思想——概念篇

461 阅读5分钟

重构

基于《重构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)