重构:一项常常被忽略的基本功
摘要: 每一个程序员都应该读的一本书。
- 原文:
- 作者:hengg
fundebug经授权转载,版权归原作者所有。
五月初的时候朋友和我说《重构》出第 2 版了,我才兴冲冲地下单,花了一个礼拜时间一口气把它读完后,才有了这篇书评。掩卷沉思,我无比赞同豆瓣网友“天心一”的评论:
这本书虽然很流行,但是应该看它而没有看的人,还是太多太多了。
一个老读者的自白
作为一个开发者,2012年初识本书的时候,我在写 java;2019年本书再版,我在写 javascript。真是应了那句老话儿:“凡是可以用 javascript 来写的应用,最终都会用 javascript 来写。”
javascript 特别适合重构,因为它很容易写的无法维护。
当然这只是个玩笑,实际上作者也解释过:重构背后的理念和架构适用于任何编程语言,选择 javascript 只是因为它应用的比较广泛。无论使用哪种编程语言都可以写出优秀的或者糟糕的代码,同样也都可以以本书的思路和技巧进行重构。
使用 javascript 展示代码范例,并不意味这本书中介绍的技巧只适用于javascript。
对比新旧两版,作者“重构”了这本书:前几章有所扩展,后几章结构调整较大,移除了原来的 12-14 章。总的来说,重构后的第 2 版更接地气、更适应时代:不再有“大型重构”,更多地聚焦操作的细节。
“fowler 先生不仅没有拔高,反而把功夫做得更扎实了。” —— 摘自译者序
虽然本书的副标题是“改善既有代码的设计”,但通读全书之后,我觉得这本书对于设计新系统时如何避免“坏味道”也是很有指导意义的。
重构和敏捷开发是一对亲兄弟
提重构就不能不提敏捷开发,马丁·福勒本身就是敏捷开发的发起者之一。敏捷作为“当红炸子鸡”,与重构有着很多相似的地方。
一是,这两者都容易成为“挂羊头,卖狗肉”中的“羊头”,很多情况下,所谓的重构就是抽出时间来重写现有的几乎无法维护的代码,就如同很多“敏捷”只做到了“不拒绝需求变更”而没有真正做到响应变化;二是,它们实现起来都是一定难度且它们的实践过程可以是交叉的——它们都着眼于具体细节而不是空架子,都欢迎变化,都强调小步快走、持续改进;三是,敏捷开发很重要的两个环节就是设计与重构,两者相辅相成,彼此互补,在实践的过程中保持较强的适应力。
重构的技巧
可以说,我在重构过程中遇到的问题大多都能在本书中找到答案。
我们看看作者对重构的定义:
重构(名词): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词): 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
为何重构、如何重构、重构的原则与手法,都可以在这本书中找到。从第 5 章起作者提供了多达 300 页的重构名录、60 余项重构的具体技巧(老版本是 70 多项,新版本移除了大规模项目的重构)。我觉得这一份非常详尽的重构手法清单更接近于字典,适合粗读之后在用到的时候再具体查阅。
至于什么时候能够用到这份名录,作者在第 3 章也有介绍:当代码有了“坏味道”就可以着手进行重构了。所谓“坏味道”,我认为并非是一程不变的准则,而是需要根据团队、项目、采用的技术栈等各方面综合得出的一种无法定量描述的经验。所以,作者用了“味道”这样一种体验来代指需要重构的地方。在作者列出的每种“坏味道”中,都给出了对应的重构手法。虽然作者罗列的 20 多种“坏味道”覆盖面很广,但是你和你的团队仍然可以总结出自己的经验来指导重构。实际上,与第 1 版相比,第 2 版中的“坏味道”增加了“神秘命名”“全局数据”“循环语句”,删除了“不完美的库类”。
我认为本书最重要也最容易被忽略的章节就是第 4 章——构筑测试体系。在第 4 章中,作者通过一个生产计划的示例一步一步的构建了一个完整的单元测试体系。显然,掌握单元测试是有一定成本的,这就导致有些开发者(尤其是前端领域)完全不注重单元测试。他们认为测试是qa的职责,自己只需要保证冒烟测试通过即可。然而反直觉的是,良好的单元测试不但是重构的先决条件和好帮手,而且能帮我们整理设计的思路,从而更好的写出优秀的代码。因为在写单元测试的时候,我们会假设自己是一个“代码破坏者”,思考如何破坏代码的运行、寻找那些可能出错的边界条件。单元测试的编写和运行可以在写完代码后进行,也可以在写代码之前动手。先写单元测试再写代码的技巧叫作测试驱动开发(tdd),也是敏捷开发的基石之一。关于tdd的技艺,作者的好友 kent beck 专门写了一本书,即《测试驱动开发》。
作者在第 1 章的示例中提到:“小步快走,代码永远处于可工作状态。”而且作者特意强调:“每当我要进行重构的时候,第一个步骤永远相同:我得确保即将修改的代码拥有一组可靠的测试。”
对于单元测试,我有一点小小的心得可以与大家分享:尽量编写纯函数。纯函数是没有副作用的函数,给出同样的参数值,纯函数总是返回同样的结果,它不依赖于参数以外的值。显然,纯函数更便于单元测试。
当然单元测试也不是万能的,它不可能检出所有的bug,而且单元测试集的覆盖率也是一个见仁见智的指标,具体需要写多少单元测试,覆盖多少代码,都是需要我们在开发中结合实际情况自己权衡的。无论如何,单元测试一直是一中非常重要却常常被忽视的技能。
另外,我在开发实践中坚持一个“432”的原则,供大家参考:
- 一个类包括注释代码不要超过400行;
- 一个纯函数最好不要超过30行;
- 函数内循环嵌套最多2层。
重构的现状
有些朋友对“重构”是不支持甚至是深恶痛绝的。
- 一部分开发者不愿意把精力“浪费”在重构上
他们觉得重构是“给飞行中的飞机修引擎”,有可能出现很多问题却带不来多少拿得出手的成绩;重构总是会在“不经意间”破坏原有功能,带来的麻烦很多,投入与收益完全不成比例,也很少会是面试的重点,花精力在这上面实在是费力不讨好。
- 许多leader反对盲目重构
在创业公司里基本不会有重构的呼声,原因无须赘言;而在一些大企业里,leader们也不是都喜欢重构,因为花时间重构意味着占用了开发新功能的时间,在代码还能跑起来甚至看起来跑得还不错的时候去重构无疑是画蛇添足;与重构带来的风险相比,重构带来的好处就不是那么有说服力了。
- 大部分qa对重构持谨慎的质疑态度
代码的变动意味着需要进行回归测试,而敏捷当道的时代,每个迭代中qa的关注重点都在新功能上,能够分配给回归测试的精力很有限,而在测试通过后的重构极有可能导致此次变更对qa不透明,无形中增加了上线的风险。
我认为以上几种反对重构的场景都是不恰当的重构导致的。
大家只是越来越接纳“重构”这个词,因为这个词听起来很好,有一种积极应对变化的感觉,但真正在做的还是跟以前一样,毫无规矩的修改。
在实践中,重构的要求是很高的:它需要有足够详尽的单元测试,需要有持续集成的环境,需要随时随地在“小步伐地永远让代码处于可工作状态”下去进行改善。正是因为许多项目的“重构”是在并不满足以上条件也没有经过成本估算、策略规划的情况下进行的,自然很容易导致失败。
- 水土不服
实际上,还有一部分开发者虽然认识到了重构是提升代码质量的有效手段,是诸如“在当下努力工作,以免日后有更多的活儿”此类观念的具现。然而在某种程度上说,这在当前996.icu大环境下是不适用的。关于这一点就只能见仁见智、自己衡量了。
没有银弹
最后,我想说一句: 没有银弹。
重构和设计模式一样,是对于最佳实践的提炼,是一系列技巧的集合,它不是打通任督二脉的灵丹妙药。如果你是一个有追求但却从来没有系统地了解过重构的程序员(当然我不相信世界上会有这种程序员),那你会发现,你在日常工作中不经意间已经用过了这本书中提到的各种重构手法。
重构是注重实践的技艺,仅仅了解其理念而忽视实践则有如抟沙作饭,白费心思;而企图把它当做“万金油”来解决所有问题也只会陷入不恰当重构的陷阱,最终得不偿失。只有在合适的场景下恰当的实践,才会实现其应有的价值。