代码重构相关内容
代码重构
事情的起因是在去年下半年,我们终于无法承受往年的历史包袱而决定开始进行代码重构。
在以前我们尝试过进行代码重构但是从来没有系统性的考虑过如何重构。在对代码重构的过程中很多经验都是来自《重构:改善既有代码的设计》这本书,里面介绍的很多技巧在实际中有些技巧非常有用,而有些技巧并不是那么容易操作的。我会陆续把实际重构时用到的技巧在后面文章中列出来
为什么要重构
为什么要重构?很多时候在改了无数个BUG后,在某次简单的业务调整却需要修改几十个文件后。估计很多人都会有个想法这沙雕代码推倒重来算了。代码能跑就不要动、代码和程序员能跑一个就可以。
这些调侃大家都听过,但是实际上如果能遇见一个不会对旧功能进行调整的项目那可太难了。很多时候我们不得不在这些屎山代码里继续开发。直到有一天这部分内容再也无法扩展了,我们不得不尝试重构。
怎么去重构
很多时候在改代码改的头疼的时候,我们只是想要一个更好的代码。但是真的尝试去写更好的代码时,又会产生手足无措。因为重构代码前我们还需要有很多工作需要准备。首先一条是我们为什么要重构,或者说重构的目标是什么?
我工作中尝试开始重构代码的时候通常会从下面的角度去考虑。
是否能够改进我的业务逻辑
很多时候相比最初的设计,随着功能越来越多,业务设计会进行膨胀,可能业务只是膨胀了一点点,但是后端的代码却成倍的增加。随着经手的开发人员越来越多,很多时候后来的开发者无法理解之前开发者的逻辑,只能在后续的开发中通过重新实现或者复制一份之前代码来保证代码逻辑在自己负责的那一部分是正确的。日积月累之下会发现一个大的业务中,某些操作被重复的执行。而重构就是为了消除这部分内容。
是否让我的代码能够被更多其他同事所理解
代码是开发者写的,是给机器使用的,但最终代码是给别人看的。随着时间流逝很多开发者连自己写的代码都无法理解,更别说去理解其他开发者的代码。如果一段代码谁也无法理解,它就像是一个泥潭,拉扯着整个业务的扩展。重构就是为了清晰代码逻辑,让这些无法理解的逻辑变得可以被理解。
如果某处混乱的代码经常出问题是不是需要重构
很多时候业务设计的范围比用户操作习惯还要广。一些边缘功能被人忽视,而在进行某些涉及相关功能的调整时,可能是开发者的疏忽、可能是文档的缺失,也可能是测试单元的缺失导致这些功能没有被调整。而这些边缘功能就像床底下的灰尘会伴随着业务很久。而整理逻辑的过程,就像是打扫屋子的过程,将这些隐秘的问题一一发现
这部分也会现在、未来会有很多扩展,现有代码是否能够支撑
随着功能增加,系统里面逻辑相互耦合增多。我们会发现当新增一个功能的时间消耗会越来越多。而且开发新功能后出现的BUG也越来越多。而且随着经手的开发者增加,我们越来越难以从代码中看到业务的真实脉络。这就像是在一个沼泽地中前行,你看到的都是一片平坦,但是踩下去的都是一个一个深坑。目标明明不远但是走的却是非常艰难。
重构的时机
想清楚目标后就可以开始为目标准备计划。并非对已经上线的代码进行调整才是重构。重构可以是专门安排的时间,也可以是每时每刻。
非计划的重构
工作中我们很难去申请到专门的时间去进行代码重构,但是随着时间代码质量又在下降,所以大部分的重构都发生在平时。一般在开发新功能或者修复BUG的时候,对代码进行重构。这个时候代码重构的效率最高,一方面是代码并未真正上线一些调整都还是可能得,另外一方面刚完成的逻辑这个时候记忆最清晰。在提交代码前再浏览一下自己的代码,就像是写论文后在最终截止的时候从头到尾再读一遍自己写的东西总是能发现一些问题的。
代码评审
代码评审也是重构的一个好时机,代码评审可以帮助团队中经验的传播,一些有经验的开发者可以将开发经验传播给经验欠缺的人。并且可以让更多人了解到新功能的实现逻辑。更重要的是通过别人角度我可以意识到哪些逻辑对于我来说是清晰的,对于其他人来说是模糊的,通过收集这些反馈,对于如果写出可以被他人理解的代码大有裨益。并且通过互相交流,开发团队中最终可以形成一个统一的代码风格,这样对于理解团队其他开发者逻辑有所帮助。
计划性重构
很多时间比较长的项目中存在一些庞大且无法理解的核心代码。这些代码所处的位置非常重要,其逻辑非常复杂且不可理解。这些代码无法通过日常进行调整。这个时候我们需要专门安排时间和开发人员对代码进行重构。计划性重构中这部分逻辑应该也是高优先级的内容。
不要重构
并非所有烂代码都需要重构的。在资源很紧张的时候,这部分内容并没有成为业务前进上的绊脚石,这部分逻辑可以被保留下来。我们要把时间留给更重要的内容上。另外一种情况是相比重构,重写似乎更容易一些,这个时候就不要再去重构了。但是这是一个非常大胆的决定。这个时候你需要充分理解这段代码的业务逻辑,并且有足够的测试保证重写后的逻辑正常。
重构的风险
很多时候我们以为重构面对的问题是:什么是坏代码、如何调整他们。但是实际上开始动手会发现面对的挑战更加复杂。重构过程中会出现一些其他模块的限制而产生妥协,重构过程中被不断扩大的边界,重构后代码是否准确以及不断被调整的开发计划。
时间,最需要的是时间,最少的也是时间
重构最大的问题就是挤占了新功能的开发时间。重构不仅没有产生新的功能,伴随着重构出现的BUG,会消耗大量的开发时间。实际开发中经常遇见“时间紧先提供一个临时方案,后续有时间后再优化”,“这是一个临时的功能,并不会成为正式功能”这些说辞,这无疑给重构带来巨大挑战。然后随着这类修改的增加,开发新功能变得非常慢。而重构就是为了提高开发效率,虽然没有实际产生新的功能,但是对于后续新功能的开发是节约了时间的。提高后续开发效率,这也是重构的最终目标。我们之前说的优化逻辑、让代码易于理解都是为了这个最终目标服务的。
欠缺的单元测试
足够的测试是重构的安全保证,但是很遗憾,至少我接触的项目中,大多数测试都是严重不足的。单元测试是一个很麻烦的工作,尤其是一个覆盖率足够高的单元测试,其编写消耗的时间甚至比开发功能需要的时间还要多的多。有些比较懒的开发者在添加一些功能的时候有意无意的忽略掉单元测试添加和编写。这些行为都导致重构这些遗留代码的时候我们无法预估最终的结果是否和以前保持一致。所以很多时候在重构之前我们需要将这些功能的测试补充进来。这无疑大大增加了重构的工作量。
不断扩大的边界,不断妥协的修改
重构之前需要明确这次重构的目的和边界。如果不能明确目的和边界,整个重构产生的工作量则无法估计。在重构某个业务的时候可能会发现其他业务代码存在相同的问题,这个时候如果盲目的修改结果可能发现修改的内容越来越多,越来越偏离最开始的目标。另外一个干扰地方就是性能,很多时候按照规则去重构会发现这回导致性能下降。我个人经验是如果不是下降的非常夸张则不需要在意。另外在进行代码重构的会发现有些逻辑可以进行性能优化,如果它不会非常影响重构进度的话可以尝试进行优化。如果明显影响了重构的进度,那么这个时候我建议是,记录但是不进行修改。需要明确重构和性能优化的区别,重构是为了提高后续开发效率,一切以这个为目标。
重构的计划
当我们明确了重构的目标、也申请到了重构的时间准备面对各种挑战。就要开始准备重构的计划了。
分析代码
在尝试重构前我们需要足够了解需要被重构的内容。包括:这段代码的作用、其上层对应了哪些业务,它底层涉及那些数据。只有真正的了解这部分业务才能保证重构的业务和之前保持一致。
定位代码中的问题。重构的最好的方式还是在原基础上进行优化。除非真的无法挽回尽量不要重写。那么进行优化就需要找到那些问题代码,并确定改进方案。
设计重构方案
根据上面的分析开始指定代码重构的计划,这部分需要考虑:
- 代码应该使用哪种方式进行重构。
- 目前的资源能够重构那些内容。
- 一个大的重构计划拆分为多个小的里程碑,这样能够阶段性的检查重构成果。
关于重构方式,我实际经验中我习惯将重构分为三种,根据重要性进行排序结果是:可读性重构、可维护性重构、性能重构。
可读性重构是最容易启动的,也是最重要的。当代码需要进行可读性重构的时候证明这块代码在阅读上都已经出了问题。而重构代码的前提是需要理解代码,而开发者是否真的完全了解其逻辑,那很难给一个确实的回答。
可维护性重构主要是针对代码的结构是否支持后续业务扩展,是否是BUG频出的地方。可维护性重构真真正正需要对代码逻辑进行调整,针对有些问题甚至需要进行代码结构上的变化。这部分也是重构比较耗时的内容。
性能重构和性能优化还是不同的。性能优化需要对数据存储和访问进行结构调整,而大多数时候重构是针对可读性和可维护性进行调整,代码重构中性能并非其目标(当然实际上因为代码更加简洁大部分时间会让代码效率更高)。但是有些时候我们面对低效率的代码的调整也会让性能提高,比如将循环中出现的数据库访问操作放在循环外、低效率的循环判断等等。这些调整并非为了提高可读性和可维护性,但是考虑到其只需要对一小段代码进行调整就能实现性能的提高,我觉得这些内容应该也被认为是代码重构的目标之一。
推荐书籍
关于重构的书籍有很多,而且这些书籍都很有用。和一些开发组件类书籍不同,这些书籍并不会时间流失而变得过时。所以可以买来是不是的看一看,每次都有会新的体会。而我在工作中很多对重构最开始的理解都是源于这些书籍。
《重构:改善既有代码的设计》(Martin Fowler):这本书是代码重构的经典之作,全面介绍了重构的概念、方法和技巧,提供了大量的实例和案例,是一本不可多得的参考书。
《代码整洁之道》(Robert C. Martin):这本书介绍了如何写出干净、优雅、可维护的代码,涵盖了很多代码重构的技巧和方法,讲解深入浅出,适合各个层次的开发人员。