重构的定义来看,重构实际上是对我们学习的经典设计思想、设计原则、设
计模式、编程规范的一种应用。重构实际上就是将这些理论知识,应用到实践的一个很好的
场景,能够锻炼我们熟练使用这些理论知识的能力。除此之外,平时堆砌业务逻辑,你可能
总觉得没啥成长,而将一个比较烂的代码重构成一个比较好的代码,会让你很有成就感。
除此之外,重构能力也是衡量一个工程师代码能力的有效手段。所谓“初级工程师在维护代
码,高级工程师在设计代码,资深工程师在重构代码”,这句话的意思是说,初级工程师在
已有代码框架下修改 bug、修改添加功能代码;高级工程师从零开始设计代码结构、搭建
代码框架;而资深工程师为代码质量负责,需要发觉代码存在的问题,重构代码,时刻保证
代码质量处于一个可控的状态(当然这里的初级、高级、资深只是一个相对概念,并不是一
个确定的职级)。
1. 重构的目的:为什么重构(why)?对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地
步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它
是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场。
2. 重构的对象:重构什么(what)?
按照重构的规模,我们可以将重构大致分为大规模高层次的重构和小规模低层次的重构。大
规模高层次重构包括对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件等
等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式。小规模低层
次的重构包括规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细
节问题,主要是针对类、函数级别的重构。小规模低层次的重构更多的是利用编码规范这一
理论知识。
3. 重构的时机:什么时候重构(when)?
我反复强调,我们一定要建立持续重构意识,把重构作为开发必不可少的部分,融入到日常
开发中,而不是等到代码出现很大问题的时候,再大刀阔斧地重构。
4. 重构的方法:如何重构(how)?
大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代
码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,
只要你愿意并且有时间,随时随地都可以去做。
如何重构代码、保证重构代码的可使用:
单元测试
实际上,写单元测试本身不需要什么高深技术。它更多的是考验程序员思维的缜密程度,看
能否设计出覆盖各种正常及异常情况的测试用例,来保证代码在任何预期或非预期的情况下
都能正确运行。
比如
toNumber
1、如果字符串只包含数字:“123”,toNumber() 函数输出对应的整数:123。
2、如果字符串是空或者 null,toNumber() 函数返回:null。
3、如果字符串包含首尾空格:“ 123”,“123 ”,“ 123 ”,toNumber() 返回对应的
整数:123。
4、如果字符串包含多个首尾空格:“ 123 ”,toNumber() 返回对应的整数:123;
5、如果字符串包含非数字字符:“123a4”,“123 4”,toNumber() 返回 null;
为啥要写单元测试:
1. 单元测试能有效地帮你发现代码中的 bug
尽管我就职的很多公司,其开发模式都是“快、糙、猛”,对单元
测试根本没有要求,但我还是坚持为自己提交的每一份代码,都编写完善的单元测试。得益
于此,我写的代码几乎是 bug free 的。这也节省了我很多 fix 低级 bug 的时间,能够有时
间去做其他更有意义的事情,我也因此在工作上赢得了很多人的认可。可以这么说,坚持写
单元测试是保证我的代码质量的一个“杀手锏”,也是帮助我拉开与其他人差距的一个“小
秘密”。
2. 写单元测试能帮你发现代码设计上的问题
前面我们提到,代码的可测试性是评判代码质量的一个重要标准。对于一段代码,如果很难
为其编写单元测试,或者单元测试写起来很吃力,需要依靠单元测试框架里很高级的特性才
能完成,那往往就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函
数、全局变量、代码高度耦合等。
3. 单元测试是对集成测试的有力补充
程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。
而大部分异常情况都比较难在测试环境中模拟。而单元测试可以利用下一节课中讲到的
mock 的方式,控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况
的表现。
除此之外,对于一些复杂系统来说,集成测试也无法覆盖得很全面。复杂系统往往有很多模
块。每个模块都有各种输入、输出、异常情况,组合起来,整个系统就有无数测试场景需要
模拟,无数的测试用例需要设计,再强大的测试团队也无法穷举完备。
尽管单元测试无法完全替代集成测试,但如果我们能保证每个类、每个函数都能按照我们的
预期来执行,底层 bug 少了,那组装起来的整个系统,出问题的概率也就相应减少了。
4. 写单元测试的过程本身就是代码重构的过程
上一节课中,我们提到,要把持续重构作为开发的一部分来执行,那写单元测试实际上就是
落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有的问题都想清
楚。而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以
发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边
界条件处理不当)等,然后针对性的进行重构。
5. 阅读单元测试能帮助你快速熟悉代码
阅读代码最有效的手段,就是先了解它的业务背景和设计思路,然后再去看代码,这样代码
读起来就会轻松很多。但据我了解,程序员都不怎么喜欢写文档和注释,而大部分程序员写
的代码又很难做到“不言自明”。在没有文档和注释的情况下,单元测试就起了替代性作
用。单元测试用例实际上就是用户用例,反映了代码的功能和如何使用。借助单元测试,我
们不需要深入的阅读代码,便能知道代码实现了什么功能,有哪些特殊情况需要考虑,有哪
些边界条件需要处理。6. 单元测试是 TDD 可落地执行的改进方案
测试驱动开发(Test-Driven Development,简称 TDD)是一个经常被提及但很少被执行
的开发模式。它的核心指导思想就是测试用例先于代码编写。不过,要让程序员能彻底地接
受和习惯这种开发模式还是挺难的,毕竟很多程序员连单元测试都懒得写,更何况在编写代
码之前先写好测试用例了。
我个人觉得,单元测试正好是对 TDD 的一种改进方案,先写代码,紧接着写单元测试,最
后根据单元测试反馈出来问题,再回过头去重构代码。这个开发流程更加容易被接受,更加
容易落地执行,而且又兼顾了 TDD 的优点。
如何编写单元测试?
前面在讲什么是单元测试的时候,我们举了一个给 toNumber() 函数写单元测试的例子。
根据那个例子,我们可以总结得出,写单元测试就是针对代码设计覆盖各种输入、异常、边
界条件的测试用例,并将这些测试用例翻译成代码的过程。
在把测试用例翻译成代码的时候,我们可以利用单元测试框架,来简化测试代码的编写。比
如,Java 中比较出名的单元测试框架有 Junit、TestNG、Spring Test 等。这些框架提供
了通用的执行流程(比如执行测试用例的 TestCaseRunner)和工具类库(比如各种
Assert 判断函数)等。借助它们,我们在编写测试代码的时候,只需要关注测试用例本身
的编写即可。
1. 写单元测试真的是件很耗时的事情吗?
尽管单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗
时。毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。不同测试
用例之间的代码差别可能并不是很大,简单 copy-paste 改改就行。
2. 对单元测试的代码质量有什么要求吗?
单元测试毕竟不会在产线上运行,而且每个类的测试代码也比较独立,基本不互相依赖。所
以,相对于被测代码,我们对单元测试代码的质量可以放低一些要求。命名稍微有些不规
范,代码稍微有些重复,也都是没有问题的。
3. 单元测试只要覆盖率高就够了吗?
单元测试覆盖率是比较容易量化的指标,常常作为单元测试写得好坏的评判标准。有很多现
成的工具专门用来做覆盖率统计,比如,JaCoCo、Cobertura、Emma、Clover。覆盖率
的计算方式有很多种,比较简单的是语句覆盖,稍微高级点的有:条件覆盖、判定覆盖、路
径覆盖。
4. 写单元测试需要了解代码的实现逻辑吗?
单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。我们切
不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。否则,一旦对代码
进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,那原本的单元
测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初
衷。
5. 如何选择单元测试框架?
写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。在公司内部,起码团
队内部需要统一单元测试框架。如果自己写的代码用已经选定的单元测试框架无法测试,那
多半是代码写得不够好,代码的可测试性不够好。这个时候,我们要重构自己的代码,让其
更容易测试,而不是去找另一个更加高级的单元测试框架。
尽管单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗
时。毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。不同测试
用例之间的代码差别可能并不是很大,简单 copy-paste 改改就行。
2. 对单元测试的代码质量有什么要求吗?
单元测试毕竟不会在产线上运行,而且每个类的测试代码也比较独立,基本不互相依赖。所
以,相对于被测代码,我们对单元测试代码的质量可以放低一些要求。命名稍微有些不规
范,代码稍微有些重复,也都是没有问题的。
3. 单元测试只要覆盖率高就够了吗?
单元测试覆盖率是比较容易量化的指标,常常作为单元测试写得好坏的评判标准。有很多现
成的工具专门用来做覆盖率统计,比如,JaCoCo、Cobertura、Emma、Clover。覆盖率
的计算方式有很多种,比较简单的是语句覆盖,稍微高级点的有:条件覆盖、判定覆盖、路
径覆盖。
4. 写单元测试需要了解代码的实现逻辑吗?
单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。我们切
不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。否则,一旦对代码
进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,那原本的单元
测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初
衷。
5. 如何选择单元测试框架?
写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。在公司内部,起码团
队内部需要统一单元测试框架。如果自己写的代码用已经选定的单元测试框架无法测试,那
多半是代码写得不够好,代码的可测试性不够好。这个时候,我们要重构自己的代码,让其
更容易测试,而不是去找另一个更加高级的单元测试框架。
对于单元测试。经常会出现锁,还有rpc框架的问题。所以我们如果采用的是直接代码写死的方式。这种方式就会出现单元测试很难,可测试性很低,这种情况就需要我们提供或者重构代码,比如我们经常说的控制反转。也就是说,上层服务才是我们需要代码测试最好的方式。也就是我们经常说的依赖注入的方式,这种方式我们可以修改底层代码的传入参数来控制rpc或者redis分布式锁的实现流程。这种代码的可测试性很好。
未决行为
全局变量
静态方法
复杂继承
高耦合代码