重构 改善既有代码的设计

77 阅读8分钟

一、初识重构

1. 什么是重构

这里先给重构下一个定义:改善既有代码的设计。

重构具体来说就是在不改变代码功能行为的情况下,对代码内部结构的一种调整。需要注意的是,重构不是代码优化,重构注重的是提高代码的可理解性与可扩展性,增强代码的健壮性,对性能的影响可好可坏。而性能优化则让程序运行的更快,当然,最终的代码相比性能优化之前可能更难理解和维护,所以这两者之间并不能直接画等号。

在这一过程中,我们秉持一个原则:不以牺牲可维护性和可理解性来换取微乎其微的性能提升。性能优化固然重要,但只有在真正影响用户体验时才会作为重点,而代码的清晰与健壮才是保证系统长久健康演进的根基。

2. 为什么要重构

2.1. 改善程序的内部设计

如果没有重构,在软件不停的版本迭代中,代码的设计只会越来越腐败,导致软件开发寸步难行。 这里的原因主要有两点:

人们只为了短期目的而修改代码时,往往没有完全理解整体的架构设计(在大项目中常有这种情况,比如在不同的地方,使用完全相同的语句做着同样的事情),代码就会失去自己的结构,代码结构的流失具有累积效应,越难看出代码所代表的设计意图,就越难保护其设计。 我们几乎不可能预先做出完美的设计,以面对后续未知的功能开发,只有在实践中才能找到真理。

所以想要体面又快速的开发功能,重构必不可少。

2.2. 使得代码更容易理解

在开发中,我们需要先理解代码在做什么,才能着手修改,很多时候自己写的代码都会忘记其实现,更不论别人的代码。可能在这段代码中有一段糟糕的条件判断逻辑,又或者其变量命名实在糟糕又缺少注释,需要花上好一段时间才能明白其真正用意。 合理的重构能让代码“自解释” ,以方便理解,无论对于协同开发,还是维护先前自己实现的功能,重构对代码的开发有着立竿见影的效果。

二、识别代码的坏味道

之前的文章中,我们讲述了重构的概念以及为什么代码需要重构,接下来,我们将进一步深入,探讨如何代码中识别一些常见的"坏味道"(即那些可能导致代码质量下降、难以维护的迹象)以及如何对这些"坏味道"的代码实施重构。

1. 重复代码 & 过长函数

重复代码: 很多时候我们会在不同的地方写下相似的代码,又或者拷贝一份副本至当前上下文中,它们之间的差异寥寥无几。

这时会出现一个很棘手的问题,当需要去修改其中的功能时,你必须找出所有的副本一一修改,这让人在阅读和修改代码时都很容易出现纰漏,再修改时每漏掉一个地方,就意味着一个Bug的诞生,所以我们要拒绝重复造轮子,尽量实现可复用性的代码。

那么可以肯定:设法将它们合而为一,程序会变得更好,我们可以将其抽离成一个公共函数,并以其功能作为命名。

过长函数: 过长函数是指一个函数的代码量过大,超出了合理的范围。一个过长的函数通常包含了太多的逻辑、分支和操作,使得函数难以理解、测试和维护。随之而来的还有高耦合性,大量的业务代码堆积在同一个方法中,如果某一处和其他逻辑耦合的代码需要进行变动,那可能导致各种意料之外的Bug。过长的函数也违反了单一职责原则,一个函数应该只做一件事情并做好。

目前普遍认为代码的行数不要超出一个屏幕的范围,因为这样会造成上下滚动,会增大出错的概率,而且是精炼越好。

三、解决代码的坏味道

1. 提炼函数

每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。

在早期的编程语言中,子程序调用需要额外开销,这使得人们不太乐意提炼函数。现代编程语言几乎已经完全免除了进程内的函数调用开销。

1.1 什么是提炼函数:

  • 浏览一段代码,理解其作用,并且发现这段代码可以提取出来
  • 将这段代码放进一个独立函数中,并让函数名称解释该函数的用途

1.2 什么时候需要提炼函数:

  • 函数过长
  • 代码重复
  • 难以理解,你需要花时间浏览一段代码才能弄清它到底在干什么

以后再读到这段提炼完成的代码时,你一眼就能看出函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。

1.3 提炼函数的注意点:

  • 写非常小的函数,不必担心短函数的大量调用会影响性能
  • 重点关注函数的命名,函数的注释往往会提供一个好名字
  • 函数名的长度并不重要,关键是将函数的意图与实现分离

1.4 如何提炼函数:

函数的意图化表达:指的是在为函数命名时,强调函数的目的和行为,而不是过于注重具体的实现细节。函数名应该清晰地传达函数的预期行为,以便其他开发者在阅读代码时能够理解函数的用途,而无需深入了解函数内部的具体实现。这种命名的思想有助于提高代码的可读性和可维护性。通过将函数名设计得更加抽象和高层次,可以使代码更易于理解,同时也更容易适应可能的变化。

  1. 找出需要提炼成函数的代码片段
  2. 创建一个新函数,并对这个函数名实现意图化表达(以它“做什么”来命名,而不是以它“怎样做”命名)
  3. 将需要提炼的代码片段复制一份到新函数中
  4. 对提炼函数的变量进行处理(1.5)
  5. 将原函数的代码片段注释,并在此处进行提炼函数的调用
  6. 对函数功能进行测试
  7. 删除注释掉的代码片段

如果想要提炼的代码非常简单,例如只是一个函数调用,只要新函数的名称能够以更好的方式昭示代码意图,我还是会提炼它;但如果想不出一个更有意义的名称,这就是一个信号,可能我不应该提炼这块代码。不过,我不一定非得马上想出最好的名字,有时在提炼的过程中好的名字才会出现。有时我会提炼一个函数,尝试使用它,然后发现不太合适,再把它内联回去,这完全没问题。只要在这个过程中学到了东西,我的时间就没有白费。

1.5 提炼函数变量的处理:

  • 不存在变量作用域的问题

    • 如果提炼出的新函数嵌套在原函数内部,不需要进行处理
    • 如果提炼出的新函数不需要用到原函数的变量,不需要进行处理
  • 存在变量作用域的问题(提炼函数引用了作用域限于原函数,在提炼出的新函数访问不到的变量)

    • 这些作用域限于原函数的变量通常为局部变量或原函数的参数,可以作为参数传递给提炼函数
    • 如果变量是在提炼部分之外声明但只在提炼函数中被使用,就把变量声明也搬移到提炼函数中去
    • 如果变量传递给提炼函数,并且变量值被修改了,看看是否能够将提炼函数处理为一个查询,并将返回值赋值给相关变量
    • 如果想进一步简化代码,可以使用 “以查询取代临时变量” 来去掉临时变量

2. 以查询取代临时变量

2.1 什么是以查询取代临时变量:

  • 消除代码中使用的临时变量,将这个临时变量的所有引用点替换为对新函数的调用。

2.2 什么时候需要以查询取代临时变量:

  • 你的程序以一个临时变量保存某一表达式或者函数返回值的结果。

2.3 以查询取代临时变量的注意点:

  • 只适用于处理那些只被计算一次且之后不再被修改的变量
  • 保证查询函数应当是纯函数,即相同的输入始终产生相同的输出