20180127 修改代码的艺术
来自圈友@正飞
圣诞节后,再次看这本编程领域经典的书《Working Effectively with Legacy Code》,中文版叫《修改代码的艺术》。英文原版豆瓣评分9分,中文版8.2分。Amazon.com评分9.2分。 从评分上看,这是一本质量非常不错的书。中文版的书名翻译虽然少了点感觉,多了点俗气——现在书名动不动什么什么艺术,什么什么之道,甚至什么什么之禅,真是让人不知道该作何感想。但应该说,这个书名翻译得还是很准确的。
因为这本书讲的就是如何修改代码! 书的开篇讲了修改代码的四种原因:
- Adding a feature
- Fixing a bug
- Improving the design
- Optimising resource usage
对于前两种原因来说,改代码自然是理直气壮,没什么可以商量的余地。然而,对于后两种原因,通常就不是那么肯定了,甚至还有种policy叫做“If it’s not broke, don’t fix it”。这种policy之所以会出现,自然是有它的原因的。最大的原因,往往不是因为改代码需要时间,而是因为更改是有风险的,这种风险就是,你不知道你的更改是不是对的,更要命的是,你不知道你的更改会影响到其它的什么地方,会后带来什么后果。出于这种恐惧的心理,我们往往倾向于不到非不得已,就不去改代码。而这里所说的非不得已,就是添加feature和fix bugs。 然而遗憾的是,这种policy被一次又一次的证明,是行不通的,因为很快你会发现,你的每个类会写得越来越大,每个方法越来越长,结构越来越复杂,越来越难以理解,每次加一个新feature,fix一个bug都越来越困难。于是到了某个点,所有东西都必须推倒重写。这种事情在历史上发生了一次又一次。那怕不会到那一步,烂代码和烂结构也会严重的拖后正常的工作进程。
这种结局是应该被避免的,也是可以被避免的,怎么避免?那就是不要只顾着add feature和fix bugs,也要及时的“Improve the design”,也就是传说中的重构。但是话说回来,重构就会有上面提到的那些风险,说得更具体点,我们需要解决好以下三个问题:
- What changes do we have to make?
- How will we know that we’ve done them correctly?
- How will we know that we haven’t broken anything?
第一个问题,没什么好说的,取决于我们自己。对于后面两个问题,怎么办呢?那就是按这本书里面说的这么办。这本书里面说的办法,叫做“Cover and Modify”,这是一种什么样的办法呢?在回答这个问题之前,先说一下我们正常情况下是怎么处理后面的两个问题的,那就是“Edit and Pray”首先我们做一些更改(Edit),然后我们Pray我们的更改是对的,同时没有造成其它地方的破坏。这明显不是一种令人很满意的做法,它不能解决我们对于改代码所带来的那种惧怕的心理。而这本书提出的方法,“Cover and Modify”,其实就是“Cover with unit tests and then Modify”,也就是说,先把你需要Modify的地方“Cover with unit tests”,然后再Modify。
当然,“Cover and Modify”并不是一样容易的事情,不然的话,也就不需要用一整本书来讲述了——是的,这本书讲的就是在各种各样的legacy code形式结构下面,怎么样“Cover and Modify”——其中最大的一个障碍,就是处理dependancy。书中讲述了在各种各样现有的情况下,怎么样隔离dependency, 或者是“mock“ dependency的行为。自然,书中也提到了mock的概念。
目前我只看到了第一部分。书中有几个点,我觉得可以总结出来分享一下,其中包括上面所说的那些。另外还有以下的几点:
1. 为什么强调Unit Test而不是测试一整个流程的集成测试(Integrated Test)?
首先,作者并没有否认集成测试的必要性和意义,但是认为集成测试有以下的问题: Error localisation:因为IntegreatedTest是测试一整个流程的,因此如果一个集成测试跑失败了,我们很难定位失败的原因是在哪,这就相当于一个app或系统发生了bug,很多时候我们很难判断问题发生的地方是在哪里一样。然而单元测试则没有这个问题。 Execution Time:跑一次集成测试需要的时间比单元测试长很多,这是让人很不耐烦的一件事。往往最后我们就不跑了。 Coverage:集成测试很难达到单元测试的覆盖率,因为环节多了以后,需要考虑的东西就很多,想要测试到各种分支的各种条件组合就很难模拟。同时它的可扩展性也很底,改一点东西,加一点东西,都需要考虑到整个系统,复杂度上升了不是一点点。
2. 代码临时变得丑一点,是值得的
有的时候为了更安全的做一些更改,更好的Cover and Modify,我们可能需要牺牲一点代码上面的“美学”,你的代码可能会临时变得比原来更丑。这是值得的,就像做手术一下,你的身体会留下一个临时的疤痕。但里面变健康了,变漂亮了,这是值得的。等内部改造完成,你可以再通过Cover and Modify的方式,把疤痕也去掉。
3. 常见的误解:哪怕你的测试通过了,也不代表程序是正确的?
书中举了一个例子,是说测试一个扫描二维码的函数,扫描出来以后,将结果显示在一个显示屏上,测试的时候,用的显示屏对象是mock。这个时候很多人会想,“你这里用的是mock,那么即使这里的测试通过了,也不代表这个系统一定能正确work啊!不代表当你用的是真实的显示屏的时候,内容可以正确的显示出来啊!”
的确,这句话不能说是错的。但是这就有点像Divide and Conquer一样,这里的测试保证的是,这个方法是可以正确work的。这点可不是无关紧要没有意义的,因为如果这个流程我们发现了一个bug,那么这个测试就相当于告诉我们,错误不是在这里,而是在其它地方。光凭这点就可以帮我们节约大量是时间。相信我们都深有体会,对于大部分bug来说,最花时间的是,定位问题到底发生在哪,而不是找出问题所在以后,如何解决。
ZhangV:
稍有“雄心”的工程师在看到legacy code时都有重构或重写的冲动,但很多时候都是不合适的。因为都是前辈们踩坑踩出来的,里面各种大坑套小坑。但是,如果业务模型还在进化,那就必须要从长计议,开始计划了,因为系统架构也是要跟着业务模型一起进化的。否则就成了技术债,总有一天要偿还。