读《重构——改善既有代码的设计》(第二版)

分类: 阅读

这本书很有名,早就听说过,但一直没有读过。原因也很简单,总觉得自己遇到的重构需求,靠自己现有的本事都能够完成。

正巧在图书馆看到了这本书的第二版,相当新,便决定读一读。读过之后,就觉得自己,很天真。

我曾经自认为做的大部分重构,恐怕根本不是重构——而算是重写了。书中有句话:“如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构”。


这本书中最能颠覆我的认知的一点,就是重构是可以有规则的小步进行的,最终却能实现整体代码结构脱胎换骨的效果。作者像是将重构当作流水线作业,只要按照总结的规则逐个实践,最终输入的垃圾代码,就能以漂亮整洁的代码输出出来。重构是一个有体系的工程。

这样一种概念,也足够给人信心。因为我往往在面对大面积充满混乱逻辑的代码时倍感挫败,手足无措——几乎就想放弃它,重写整个系统。可是作者信誓旦旦地说他成功地实践过多次他总结的各种重构手法,并几乎总是小步前进,最终却成效显著。

这不由得让我反思我从前的做法。我似乎总是想着一步到位,把我当时所看到的一切不合理的问题立时解决掉,各种改名、搬移、封装等操作都想在一次提交完成——最终的结果就是,“代码在重构过程中有一两天时间不可用”,甚至更久——因为我已经理不清我重构过程中的代码逻辑了。这样看来,小步进行似乎是非常明智的选择,不仅进度是可控的,最终完成各类重构的总时间也很可能是更短的。

这样一种新指导思想的领悟,实在令人受益。上次有这样被感动,还是实践了某著名英语培训机构的老师传授的“一天至少5个List,两个月背5遍单词书”的背单词方法之后。


除了这一份感动,我还在这本书中收获了另一份感动——甚至要热泪盈眶了。

“怎么对经理说”,这竟是这本书中的一节话题。作者实在太贴心了!其实每一个开发者可能都会苦恼,重构这种工作很容易吃力不讨好——产品表现形式上没有任何改进,却花费了不少时间。那么怎么去跟经理讲我想花时间重构呢?

当然了,这本书告诉我们,懂技术的经理不用大家提出来,也会主动鼓励大家去做;另外的情况呢,那当然就是不必告诉经理了。

不必告诉经理为什么是可行的?作者说,重构往往会使得开发新功能所需的工时更少,那么重构便可以当作开发新功能的任务之一,也就不存在什么苦恼了。

书中也提到:“如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。”

这同时为何时重构给出了很好的建议。当然,作者还提出了其他的重构时机,比如说,我们需要读一段代码的时候,就是重构代码的好时机——如果不重构,很可能读不懂。


看到作者总在说重构,我就开始怀疑自己有时候总想重写的想法是不是合理的——确实有很多次想法现在看来并不合理,但又总觉得有些似乎并不能应用作者提到的方法论。正疑惑着,就看到了“何时不应该重构”一节。

“重写比重构还容易,就别重构了。”这倒是解答了我的疑惑。不过作者也说,“决定到底应该重构还是重写,需要良好的判断力与丰富的经验,我无法给出一条简单的建议。”这也提醒我还是要更加谨慎地决定。

作者还提到另一种情况,就是凌乱的代码隐藏在API之下,如果无需了解API的具体工作原理,就无需重构。这也很好理解,API用的好好的,就先不管怎么实现的了嘛。这似乎也类似各种神经网络黑盒子,反正就是能做好识别,管他到底怎么做到的呢!


有时,重构可能确实难以实行——往往是因为不恰当的管理模式。作者提到“有些组织喜欢给每段代码都指定唯一的所有者,只有这个人能修改这段代码”。

这种“细粒度的强代码所有制”会让接口的维护变得异常麻烦。重构时,接口往往会发生改变——改名,或者参数等都可能发生变化。通常来说,对外的接口需要提供一定的向前兼容性。但在“细粒度的强代码所有制”下,所有内部的“跨所有者”的接口也不能轻易的同时重构,哪怕接口只有一处“跨所有者”的引用,便都需要维护向前兼容性。这就造成了巨大的额外成本。

作者认为,系统各个模块的责任人应当负责的,不是阻止代码被他人修改,而是审查代码的修改。

“这种类似开源的模式常常是一个合适的折中。”


其实我觉得除了“强代码所有制”,还有一些情况会阻碍重构的进行:不少人可能会倾向于拒绝对能够正常运作的程序进行修改,抑或是倾向于使用统一推荐的方式去修改、新增代码,哪怕这一方式已经十分陈旧繁琐。

常常会听到有人说,“这段代码既然能work就不要动它”,“不要做与功能无关的修改”(如修正错误的缩进等代码风格问题)等等。在我看来,这都不是很好的工程理念。

书中也提到:“……将重构与添加新功能在版本控制的提交中分开……但我并不认同这种做法。重构常常与新添功能紧密交织,不值得花功夫把它们分开。并且这样做也使重构脱离了上下文,使人看不出这些‘重构提交’的价值。”

我们往往是按需重构,所以重构是有上下文的。就好像对代码风格的修正,往往我们只会对编辑过的代码文件进行修正。如果将重构与添加新功能的提交分开,难不成我们要有一个巨大的提交,修正了全部代码存在的风格问题嘛?

至于“统一推荐的方式”,我想这极大的阻碍了代码的创新。

好的代码结构,应该能够容忍各个解耦的模块拥有自由的添加新功能或者修改、重构代码的方式。只要能划清边界,把复杂的逻辑隐藏在API之下,想必这是能够做到的。如此一来,整个系统的拥有者就可以较少的干涉系统各个模块的开发者们。

我觉得,开发的最佳实践就是拥有最少的最佳实践,并拥抱改变。

问渠哪得清如许,为有源头活水来。


当然,想万无一失地做到上面提到的自由度,拥有完善的自动化的测试是十分必要的。对于重构而言,测试也是极为重要的。

我在写测试时,时常陷入学院派的纠结:“我写的是单元测试还是集成测试?”、“这样写覆盖的情况是不是太少了?”……

有些问题可能确实不容易给出答案,但作者说的对:“编写未臻完善的测试并经常运行,好过对完美测试的无尽等待。”

聊胜于无嘛!

“不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug。”

另外,我常在一些开源项目看到他们会专门有一些测试,使用了非常真实的数据,测试名称中也有“bug”的字样,大概是对过往发现的bug场景的测试。甚至有些开源项目直接在提issue的建议中,提到希望能够提供复现bug的测试集。

书中也对这一实践有所建议:“每当你收到bug报告,请先写一个单元测试来暴露这个bug。”


我不太喜欢写注释,因为我觉得代码本身就很清晰地说明问题。

我一直认为,代码已经描述了怎么做,而注释应该仅仅宏观地讲做什么以及为什么这么做。

书中说,“当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。”

其实这也部分支持了我的说法,不过更进一步。实际上代码也可以表达“做什么”,比如在函数名里写清楚内部的代码在做什么。这便可以减少大量注释。甚至重构之后,有些需要说明“为什么”的情形,也不再必要——因为函数变简单之后,其中的逻辑是“显然可知”的。


本书还对架构级别的改变做出了建议:“通过重构,我们能得到一个设计良好的代码库,使其能够优雅地应对不断变化的需求……应对未来变化的办法之一,就是在软件里植入灵活性机制……”

译者在这本书的中文版译序中说:“在此书尚未成型之前,我和当时ThoughtWorks的同事曾有很多猜测,猜Fowler先生(作者)是否会在第2版中拔高层次,多谈谈设计及至架构级别的重构手法,甚或跟随‘敏捷组织’‘精益企业’的风潮谈谈组织重构,也未为不可。孰料成书令我们跌破眼镜,Fowler先生不仅没有拔高,反而把工夫做的更扎实了。”

在我看来,其实作者还是提到了,并且做了回应。那就是架构的重构需要的其实是“灵活性”,是优雅易复用的代码库。因此,这本书不必涉及更多高层次的重构,而只需稳扎稳打,便自然能让做出实践的读者在更高层次的重构中依然游刃有余。


本书后面便是类似字典的各类重构的手法,每一个手法都极为简单,步伐极小——而每一个手法本身也由各种极为简单的,几乎原子的修改步骤构成。其实很多手法我都经常使用,然而不曾想到作者竟然能够整理得如此细致。

这本书值得作为一本手册随时查阅。


最后,我还是想用一句古诗来结尾:

“问渠哪得清如许,为有源头活水来。”