前段时间重读了《重构:改善代码既有设计》[1],收货颇多。于是,简单写了一篇文章来聊聊我对重构的看法。
何谓重构?
学习重构必看的一本神书《重构:改善代码既有设计》从两个角度给出了重构的定义:
- 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
用更贴近工程师的语言来说:重构就是利用设计模式(如组合模式、策略模式、责任链模式)、软件设计原则(如 SOLID 原则、YAGNI 原则、KISS 原则)和重构手段(如封装、继承、构建测试体系)来让代码更容易理解,更易于修改。
软件设计原则指导着我们组织和规范代码,同时,重构也是为了能够尽量设计出尽量满足软件设计原则的软件。
正确重构的核心在于 步子一定要小,每一步的重构都不会影响软件的正常运行,可以随时停止重构。
常见的设计模式如下 :
更全面的设计模式总结,可以看 java-design-patterns[2] 这个开源项目。
常见的软件设计原则如下 :
常见的软件设计原
更全面的设计原则总结,可以看 java-design-patterns[3] 和 hacker-laws-zh[4] 这两个开源项目。
为什么要重构?
在上面介绍重构定义的时候,我从比较抽象的角度介绍了重构的好处:重构的主要目的主要是提升代码&架构的灵活性/可扩展性以及复用性。
如果对应到一个真实的项目,重构具体能为我们带来什么好处呢?
- 让代码更容易理解 :通过添加注释、命名规范、逻辑优化等手段可以让我们的代码更容易被理解;
- 避免代码腐化 :通过重构干掉坏味道代码;
- 加深对代码的理解 :重构代码的过程会加深你对某部分代码的理解;
- 发现潜在 bug :是这样的,很多潜在的 bug ,都是我们在重构的过程中发现的;
- ......
看了上面介绍的关于重构带来的好处之后,你会发现重构的最终目标是 提高软件开发速度和质量 。
重构并不会减慢软件开发速度,相反,如果代码质量和软件设计较差,当我们想要添加新功能的话,开发速度会越来越慢。到了最后,甚至都有想要重写整个系统的冲动。
《重构:改善代码既有设计》这本书中这样说:
重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
何时进行重构?
重构在是开发过程中随时可以进行的,见机行事即可,并不需要单独分配一两天的时间专门用来重构。
提交代码之前
《重构:改善代码既有设计》这本书介绍了一个 营地法则 的概念:
编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。
这个概念表达的核心思想其实很简单:在你提交代码的之前,花一会时间想一想,我这次的提交是让项目代码变得更健康了,还是更腐化了,或者说没什么变化?
项目团队的每一个人只有保证自己的提交没有让项目代码变得更腐化,项目代码才会朝着健康的方向发展。
当我们离开营地(项目代码)的时候,请不要留下垃圾(代码花味道)!尽量确保营地变得更干净了!
开发一个新功能之后&之前
在开发一个新功能之后,我们应该回过头看看是不是有可以改进的地方。在添加一个新功能之前,我们可以思考一下自己是否可以重构代码以让新功能的开发更容易。
一个新功能的开发不应该仅仅只有功能验证通过那么简单,我们还应该尽量保证代码质量。
有一个两顶帽子的比喻:在我开发新功能之前,我发现重构可以让新功能的开发更容易,于是我戴上了重构的帽子。重构之后,我换回原来的帽子,继续开发新能功能。新功能开发完成之后,我又发现自己的代码难以理解,于是我又戴上了重构帽子。比较好的开发状态就是就是这样在重构和开发新功能之间来回切换。
refractor-two-hats
Code Review 之后
Code Review 可以非常有效提高代码的整体质量,它会帮助我们发现代码中的坏味道以及可能存在问题的地方。并且, Code Review 可以帮助项目团队其他程序员理解你负责的业务模块,有效避免人员方面的单点风险。
经历一次 Code Review ,你的代码可能会收到很多改进建议。
捡垃圾式重构
当我们发现坏味道代码(垃圾)的时候,如果我们不想停下手头自己正在做的工作,但又不想放着垃圾不管,我们可以这样做:
- 如果这个垃圾很容易重构的话,我们可以立即重构它。
- 如果这个垃圾不太容易重构的话,我们可以先记录下来,当完成当下的任务再回来重构它。
阅读理解代码的时候
搞开发的小伙伴应该非常有体会:我们经常需要阅读项目团队中其他人写的代码,也经常需要阅读自己过去写的代码。阅读代码的时候,通常要比我们写代码的时间还要多很多。
我们在阅读理解代码的时候,如果发现一些坏味道的话,我们就可以对其进行重构。
就比如说你在阅读张三写的某段代码的时候,你发现这段代码逻辑过于复杂难以理解,你有更好的写法,那你就可以对张三的这段代码逻辑进行重构。
重构有哪些注意事项?
单元测试是重构的保护网
单元测试可以为重构提供信心,降低重构的成本。我们要像重视生产代码那样,重视单元测试。
另外,多提一句:持续集成也要依赖单元测试,当持续集成服务自动构建新代码之后,会自动运行单元测试来发现代码错误。
怎样才能算单元测试呢? 网上的定义很多,很抽象,很容易把人给看迷糊了。我觉得对于单元测试的定义主要取决于你的项目,一个函数甚至是一个类都可以看作是一个单元。就比如说我们写了一个计算个人股票收益率的方法,我们为了验证它的正确性专门为它写了一个单元测试。再比如说我们代码有一个类专门负责数据脱敏,我们为了验证脱敏是否符合预期专门为这个类写了一个单元测试。
单元测试也是需要重构或者修改的。 《代码整洁之道:敏捷软件开发手册》[5]这本书这样写到:
测试代码需要随着生产代码的演进而修改,如果测试不能保持整洁,只会越来越难修改。
不要为了重构而重构
重构一定是要为项目带来价值的! 某些情况下我们不应该进行重构:
- 学习了某个设计模式/工程实践之后,不顾项目实际情况,刻意使用在项目上(避免货物崇拜编程);
- 项目进展比较急的时候,重构项目调用的某个 API 的底层代码(重构之后对项目调用这个 API 并没有带来什么价值);
- 重写比重构更容易更省事;
- ......
遵循方法
《重构:改善代码既有设计》这本书中列举除了代码常见的一些坏味道(比如重复代码、过长函数)和重构手段(如提炼函数、提炼变量、提炼类)。我们应该花时间去学习这些重构相关的理论知识,并在代码中去实践这些重构理论。
如何练习重构?
除了可以在重构项目代码的过程中练习精进重构之外,你还可以有下面这些手段:
- 重构实战练习[6] :通过几个小案例一步一步带你学习重构!
- 设计模式 重构学习网站[7] :免费在线学习代码重构、 设计模式、 SOLID 原则 (单一职责、 开闭原则、 里氏替换、 接口隔离以及依赖反转) 。
参考资料
[1]《重构:改善代码既有设计》: https://book.douban.com/subject/30468597/
[2]java-design-patterns: https://github.com/iluwatar/java-design-patterns
[3]java-design-patterns: https://github.com/iluwatar/java-design-patterns
[4]hacker-laws-zh: https://github.com/nusr/hacker-laws-zh
[5]《代码整洁之道:敏捷软件开发手册》: https://book.douban.com/subject/4199741/
[6]重构实战练习: https://linesh.gitbook.io/refactoring/
[7]设计模式 重构学习网站: https://refactoringguru.cn/