一、代码重构的技术手段:单元测试
1. 什么是单元测试?
单元测试是开发自己编写的用于测试自己代码正确性的代码。相比于集成测试,单元测试更注重类和函数的逻辑是否按照预期执行了,是属于代码层级的测试。
2. 为什么要写单元测试?
- 发现你代码中的bug
- 发现代码设计上的问题,如果单元测试写起来很吃力,那么极有可能代码设计的是不够合理的
- 对集成测试的有力补充,尤其是一些边界条件的测试
- 写单元测试的过程本身就是代码重构的过程
- 阅读单元测试能够帮你快速熟悉代码,实际上单元测试就是用户用例,反映了代码的功能和如何使用
- 单元测试是TDD可落地执行的改进方案(测试驱动开发)
- ....
3. 如何编写单元测试?
这里总结出了一些经验:
(1)写单元测试真的很耗时吗?
确实,过程很繁琐,但是基本都是cv。
(2)对单元测试的代码质量有什么要求吗?
不会产线运行,也不会相互依赖,可以放低要求。
(3)单元测试只要覆盖率高就够了吗?
60到70即可,不必过度追求单元测试覆盖率。
(4)写单元测试需要了解代码的实现逻辑吗?
要着重关心被测函数实现的功能而非内部逻辑。
(5)如何选择单元测试框架?
不需要太复杂的技术,大部分单元测试框架都能满足。
4. 如何在团队中推行单元测试?
首先,100%落实执行单元测试是一件“知易行难”的事。
很多历史代码因为没有单元测试而难以推行,需要每个开发都要有主人翁意识。
此外,程序员应该是智力密集型行业,但是目前很多都是劳动密集型,既没有单元测试,也没有Code Review。写好代码直接提交,然后交给黑盒测试,然后再改bug,如此反复。但实际上,在完善单元测试的情况下,可以很大的减少黑盒测试的投入。
关于单元测试的更多详细信息,可以参考之前的博客中关于单元测试我在团队中的一些实践。
二、代码的可测试性
1. 什么是代码的可测试性?
粗略地讲,所谓代码的可测试性,就是针对代码编写单元测试的难易程度。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架中很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。
2. 编写可测试性代码的最有效手段
依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑战的地方。
3. 常见的 Anti-Patterns
反面模式(anti-pattern或antipattern)指的是在实践中经常出现但又低效或是有待优化的设计模式。
常见的测试不友好的代码有下面这 5 种:
- 代码中包含未决行为逻辑
- 滥用可变全局变量
- 滥用静态方法
- 使用复杂的继承关系
- 高度耦合的代码
三、如何通过封装、抽象、模块化、中间层等解耦代码?
前面讲到,重构可以分为大规模高层重构和小规模低层次重构。大型重构是对系统、模块、代码结构、类之间关系等顶层代码设计进行的重构。对于大型重构来说,最有效的一个手段莫过于“解耦”。
下面分三个部分详细说一下这个“解耦”:
1. “解耦”为何如此重要?
如果说重构是保证代码质量不至于腐化到无可救药地步的有效手段,那么利用解耦的方法对代码重构,就是保证代码不至于复杂到无法控制的有效手段。
“高内聚、松耦合”是一个比较通用的设计思想,不仅可以知道细粒度的类和类之间关系的设计,还能指导粗粒度的系统、架构、模块的设计。相对于编码规范,它能够在更高层次上提高代码的可读性和可维护性。“高内聚、松耦合”的特性可以让我们在阅读和修改代码的时候专注在某一模块,修改代码不会牵一发而动全身。
除此之外,代码“高内聚、松耦合”,也就意味着,代码结构清晰、分层和模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。即便某个具体的类或者模块设计得不怎么合理,代码质量不怎么高,影响的范围是非常有限的。我们可以聚焦于这个模块或者类,做相应的小型重构。而相对于代码结构的调整,这种改动范围比较集中的小型重构的难度就容易多了。
2. 代码是否需要“解耦”?
判断这个问题的标准有很多,比如说看看这个代码修改后的影响范围,看代码修改会不会牵一发而动全身。此外,还有一个直接的衡量标准,那就是把模块与模块之间、类与类之间的依赖关系设计出来,根据关系图的复杂性来判断是否需要解耦。
3. 如何给代码“解耦”?
(1)封装与抽象
封装和抽象作为两个非常通用的设计思想,可以应用在很多设计场景中,比如系统、模块、lib、组件、接口、类等等的设计。封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。
(2)中间层
引入中间层能简化模块或类之间的依赖关系。下面这张图是引入中间层前后的依赖关系对比图。在引入数据存储中间层之前,A、B、C 三个模块都要依赖内存一级缓存、Redis 二级缓存、DB 持久化存储三个模块。在引入中间层之后,三个模块只需要依赖数据存储一个模块即可。从图上可以看出,中间层的引入明显地简化了依赖关系,让代码结构更加清晰。
(3)模块化
模块化是构建复杂系统常用的手段。不仅在软件行业,在建筑、机械制造等行业,这个手段也非常有用。对于一个大型复杂系统来说,没有人能掌控所有的细节。之所以我们能搭建出如此复杂的系统,并且能维护得了,最主要的原因就是将系统划分成各个独立的模块,让不同的人负责不同的模块,这样即便在不了解全部细节的情况下,管理者也能协调各个模块,让整个系统有效运转。
(4)其他设计思想和原则
- 单一职责原则
- 基于接口而非实现编程
- 依赖注入
- 多用组合少用继承
- 迪米特法则
具体的内容可以跳转到前面设计思想的相关章节查看。