五、代码重构
重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。
重构的目的(why)、对象(what)、时机(when)、方法(how);
为什么要重构(why)?
对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。
对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场。
到底重构什么(what)?
大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。
小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。你只需要熟练掌握各种编码规范,就可以做到得心应手。
什么时候重构(when)?
持续重构;在修改、添加某个功能代码的时候,顺手把不符合编码规范、不好的设计重构一下。
如何重构(how)?
进行大型重构的时候,我们要提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。只有这样,我们才能让每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。
小规模低层次的重构,因为影响范围小,改动耗时短,只要你愿意并且有时间,随时随地都可以去做。
##保证重构不出错的技术手段:单元测试和代码的可测试性;
为什么要写单元测试?
- 单元测试能有效地帮你发现代码中的 bug
- 写单元测试能帮你发现代码设计上的问题
- 单元测试是对集成测试的有力补充
程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。而大部分异常情况都比较难在测试环境中模拟。
对于一些复杂系统来说,集成测试也无法覆盖得很全面。复杂系统往往有很多模块。每个模块都有各种输入、输出、异常情况,组合起来,整个系统就有无数测试场景需要模拟,无数的测试用例需要设计,再强大的测试团队也无法穷举完备。
- 写单元测试的过程本身就是代码重构的过程
写单元测试实际上就是落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有的问题都想清楚。而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。
- 阅读单元测试能帮助你快速熟悉代码
借助单元测试,我们不需要深入的阅读代码,便能知道代码实现了什么功能,有哪些特殊情况需要考虑,有哪些边界条件需要处理。
- 单元测试是 TDD 可落地执行的改进方案
先写代码,紧接着写单元测试,最后根据单元测试反馈出来问题,再回过头去重构代码。这个开发流程更加容易被接受,更加容易落地执行,而且又兼顾了 TDD 的优点。
编写单元测试?
-
测试用例是否覆盖了所有可能的情况
-
不依赖被测试函数的具体实现逻辑,只关心被测函数实现了什么功能
-
依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务
-
常见的 Anti-Patterns
- 未决行为
未决行为逻辑就是,代码的输出是随机或者说不确定的,比如,跟时间、随机数有关的代码。将这部分逻辑单独抽象到一个函数中
- 全局变量
- 静态方法
静态方法也很难 mock。但是,这个要分情况来看。只有在这个静态方法执行耗时太长、依赖外部资源、逻辑复杂、行为未决等情况下,我们才需要在单元测试中 mock 这个静态方法。除此之外,如果只是类似 Math.abs() 这样的简单静态方法,并不会影响代码的可测试性,因为本身并不需要 mock。
- 复杂继承
如果父类需要 mock 某个依赖对象才能进行单元测试,那所有的子类、子类的子类……在编写单元测试的时候,都要 mock 这个依赖对象。对于层次很深(在继承关系类图中表现为纵向深度)、结构复杂(在继承关系类图中表现为横向广度)的继承关系,越底层的子类要 mock 的对象可能就会越多,这样就会导致,底层子类在写单元测试的时候,要一个一个 mock 很多依赖对象,而且还需要查看父类代码,去了解该如何 mock 这些依赖对象。
- 高耦合代码
如果一个类职责很重,需要依赖十几个外部对象才能完成工作,代码高度耦合,那我们在编写单元测试的时候,可能需要 mock 这十几个依赖的对象。不管是从代码设计的角度来说,还是从编写单元测试的角度来说,这都是不合理的。
大重构(大规模高层次)
1.“解耦”为何如此重要?
过于复杂的代码往往在可读性、可维护性上都不友好。解耦保证代码松耦合、高内聚,是控制代码复杂度的有效手段。代码高内聚、松耦合,也就是意味着,代码结构清晰、分层模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。
- 代码是否需要“解耦”?
间接的衡量标准有很多,比如,看修改代码是否牵一发而动全身。直接的衡量标准是把模块与模块、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构。
- 如何给代码“解耦”?
给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些其他的设计思想与原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则等。设计模式,比如观察者模式。