Skip to content

重构

“重构”这个词对于大部分工程师来说都不陌生。但是真正进行过代码重构的不多,而把持续重构作为开发的一部分就更少了。

一方面,重构对工程师要求很高。需要洞察出代码存在的坏味道或设计的不足,并且合理、熟练的应用设计思想、原则、模式、编程规范等理论知识解决这些问题。

另一方面,很多工程师没有很好的认识重构。为什么要重构、到底重构什么、什么时候重构、该如何重构等问题理解不深。对重构没有系统性、全局性的认识。面对一堆烂代码,没有重构技巧的指导,只能想到哪改到哪,不能全面的改善代码质量。

接下来通过学习帮助对重构有更清晰的认识。

什么情况下要重构,到底重构什么,又该如何重构

什么是重构

软件设计大师 Martin Fowler 是这样定义重构的:“重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。

这个定义中值得强调的点:“重构不改变外部的可见行为”。可以把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程范式等理论来优化代码,修改设计上的不足,提高代码的质量。

重构的目的:为什么要重构(why)

了解重构定义后,我们看一下为什么要进行重构?

  • 首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。

项目演进代码堆砌之后,如果没有人为代码的质量负责任,代码总会向越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要去重构已经没人能做到了。

  • 其次,优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。

就像优秀的公司和产品也是迭代出来的。我们无法 100% 预见未来的需求,也没有足够的精力、时间、资源现在为遥远的未来买单。所以随着系统的演进,代码重构是不可避免的。

  • 最后,重构是避免过度设计的有效手段。

在维护代码的过程中,真正遇到问题的时候再对代码进行重构,能有效避免前期投入太多时间做过度设计,做到有的放矢(yǒu dì fàng shǐ 即有靶子才放箭,放箭時要對準靶子。比喻說話做事有明確的目的和針對性。)

除此之外,重构对一个工程师本身技术的成长也有重要的意义。

重构实际上是经典设计思想、设计原则、设计模式、编程规范一个很好的应用场景,能够锻炼我们对这些理论知识使用的能力。

除此之外,平常堆砌业务逻辑你可能觉得没有成长,而将比较烂的代码重构成一个比较好的代码,会让你很有成就感

重构也是衡量一个工程师代码能力的有效手段。所谓“初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码”。

重构的对象:到底重构什么(what)

根据重构的规模,可以分为大规模高层次重构(简称“大型重构”)和小规模低层次的重构(简称“小型重构”)。

  • 大型重构:指的是对顶层代码设计的重构,包括系统、模块、代码结构、类与类之间的关系等重构。

重构的手段有分层、模块化、解耦、抽象可复用组件等。重构的工具事是面学习过的那些设计思想、原则和模式。这类重构代码特点是改动量会比较多、影响会比较大,所以难度较高,耗时较长,引入 bug 的概率也相对较大。

  • 小型重构:指的是对代码细节的重构,主要是类、函数、变量等代码级别的重构。

比如规范命名、规范注释、消除超大类或函数、提取重复代码等。重构的工具是编程规范。特点是修改的地方比较集中,比较简单,耗时短,引入 bug 的风险相对也会比较小。

重构的时机:什么时候重构(when)

代码烂到一定程度之后才去重构吗?当然不是!!!

因为当代码烂到“开发效率低,招了很多人,天天加班,产出却很少。线上 bug 频发,领导发飙,中层束手无策,工程师抱怨不断,查找 bug 困难”的时候,基本上重构也无法解决问题了。

反对平时不注重代码质量,堆砌烂代码,实在维护不了了就大刀阔斧地重构、甚至重写的行为。寄希望于代码烂到一定程度之后,集中重构解决所有问题是不现实的。必须探索一条可持续、可演进的方式

特别提倡的重构策略是持续重构。平时没有事情的时候,看看项目那些写得不够好的、可以优化的代码,主动去重构一下。或者在开发时修改、添加某个功能时,可以顺手把不符合编程规范、不好的设计重构一下。

就像把单元测试、Code Review 作为开发的一部分,如果能把持续重构作为开发的一部分,成为一种开发习惯,对项目、对自己都有好处。

持续重构的意识比重构本身的能力更重要。

重构的方法:又该如何重构(how)

前面我们根据重构的规模,把重构笼统地分为大型重构和小型重构。针对这两种类型,要区别对待。

  • 进行大型重构:要提前规划好完善的重构计划,有条不紊的分阶段来进行。

具体来说:

1.每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库的代码一直处于可运行、逻辑正确的状态 2.每个阶段要控制好重构影响到的代码范围,考虑如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。

只要这样,我们才能让每一阶段的重构不至于耗时太长(最好一天完成),不至于与新的功能开发冲突。

大型重构一定是有组织、有计划,并且非常谨慎,需要有经验、熟悉业务的资深同是来主导。

  • 进行小型重构:影响小,改动耗时短,只要你愿意并且有时间,随时都可以去做。

这些低层次的质量问题,除了人工去发现,更建议借用成熟的静态代码分析工具(比如 sornar)来自动发现代码问题,然后针对性的重构优化。

对于重构这件事,资深工程师,项目 leader 要负起责任来,持续重构,时刻保持代码质量处于一个良好的状态。否则,一旦出现“破窗效应”,一个人往里面堆了一些烂代码,之后就会有更多人往里面堆更烂的代码。毕竟往项目里堆砌烂代码的成本太低了。

保持代码质量最好的方法还是打造一种好的技术氛围,以此驱动大家去关注代码质量,持续重构代码

为了保证重构不出错,有哪些非常能落地的技术手段

很多工程师对重构还是比较认同的,面对项目中的烂代码,也想重构一下,但是又担心出问题,出力不讨好

那如何保证重构不出错呢?

需要熟练掌握各种设计原则、思想、模式,还需要对业务和代码有足够的了解。除了这些个人能力因素之外,最可落地执行、最有效保证重构不出错的手段应该就是单元测试(Unit Testing)了。

当重构之后,如果新的代码仍能通过单元测试,那就说明原有的逻辑的正确性未被破坏,原有的外部可见行为未变,符合前面对重构的定义。

接下来我们学习一下单元测试。

什么是单元测试

  • 单元测试

单元测试由研发工程师自己来编写,用来测试自己写的代码的正确性。测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。是一个白盒测试

其他测试方式对比(https://start-up.house/en/blog/articles/front-end-testing):

  • 集成测试

确保应用程序的各个部分能够一起工作。从商业角度来看,它们是至关重要的。记住,用户并不关心你的应用程序的单一功能是否工作。他们感兴趣的是知道他们是否可以使用整个应用程序,这就是集成测试的闪光点。

例如 React测试库 是集成测试的首选资源。它的主要目标是允许测试你的用户使用应用程序的方式(组件),避免测试实施细节。提供了大量的工具,使测试更直接,更可维护。

  • E2E测试

与其他类型的测试不同,因为它们在真实的浏览器中运行。我们把测试用例写成自动浏览器的分步说明,以通过我们要测试的应用程序的部分。

最受欢迎的端到端测试工具之一是Cypress。它很容易设置和使用,而且速度很快。

写单元测试本身不需要什么高深技术。它更多的是考验程序员思维的缜密程度,看能否设计出覆盖各种正常及异常情况的测试用例,来保证代码在任何预期或非预期的情况下都能正确运行

为什么要写单元测试

单元测试除了能有效地为重构保驾护航之外,也是保证代码质量最有效的两个手段之一(另一个是 Code Review)

总结了以下几点单元测试的好处。尽管有些“务虚”,不过如果认真写过,应该会有共鸣。

  • 单元测试能有效地帮你发现代码中的 bug

能否写出 bug free 的代码,是判断工程师编码能力的重要标准之一。通过编写完善的单元测试,保证写的代码几乎是 bug free 的。节省了我很多 fix 低级 bug 的时间,能够有时间去做其他更有意义的事情,也因此在工作上赢得了很多人的认可。

  • 写单元测试能帮你发现代码设计上的问题

代码的可测试性是评判代码质量的一个重要标准。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很吃力,需要依靠单元测试框架里很高级的特性才能完成,那往往就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等。

  • 单元测试是对集成测试的有力补充

程序运行的 bug 往往出现在一些边界条件、异常情况下,有些测试环境比较难模拟。单元测试可以利用 mock 的方式,控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况的表现。

  • 写单元测试的过程本身就是代码重构的过程
  • 阅读单元测试能帮助你快速熟悉代码
  • 单元测试是 TDD 可落地执行的改进方案

如何编写单元测试

单元测试为何难落地执行

重构 has loaded