Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

重构的思想——概念篇 #22

Open
MyPrototypeWhat opened this issue Mar 23, 2022 · 0 comments
Open

重构的思想——概念篇 #22

MyPrototypeWhat opened this issue Mar 23, 2022 · 0 comments

Comments

@MyPrototypeWhat
Copy link
Owner

MyPrototypeWhat commented Mar 23, 2022

重构

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant