大家好,我是Z哥。
最近在整理一些项目,所以相关的文章写的多了些。之前的相关文章有《聊聊单元测试》,感兴趣的话可以点击文末链接去阅读。
这次整理项目的时候,做了比较多的codereview和重构。好久没做这么高强度了重构了,所以对重构这件事有了新的思考和理解。
突然发现叫我们程序员“码农”还挺形象的,因为写代码和种田很像,想有个好收成,就要好好管理代码,让它们井井有条。
吴军老师在《文明之光》里讲到一个「垄耕种植法」,它由中国人发明,后发扬到全球,影响了全世界的粮食生产。据说欧洲人民以前是把种子随意地撒在地里,任其自由生长,结果收成很低,如果种下20斤,大概只能收获60斤左右粮食。而中国早在先秦时期亩产最少都在240斤以上,最新的数据是今年11月初袁隆平的杂交水稻,早晚稻加起来达到3000斤,这都得益于「垄耕种植法」。
所以,当你看到那些被随意“播种”的糟糕代码,是改,还是不改?改吧,花时间;不改吧,就像上面的欧洲人民。
其实很多人对「重构」的理解还有些误区。「重构」仅仅是所谓的优化代码吗?并不是。
Martin Fowler大神在他的《重构》一书中对「重构」的定义就非常准确。
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
《重构》Martin Fowler
所以,重构不仅仅是修改代码,是对软件结构的调整,修改代码只是其中的一个手段而已。
那么具体应该怎么做呢?在这之前,需要考虑清楚以下几个问题。
/01 什么时候重构?/
很多理想主义者认为的理想情况自然是随时发现坏代码就重构。但是这里存在两个问题:
有很多客观因素导致了就算发现了坏代码,也不一定有充足的时间来修改。
不同的人代码水平不一,一个人看起来没毛病的代码,可能在其他人眼里却是需要重构的代码。
所以我们需要几个更加客观的外部标准,Z哥建议你可以从以下三个方面来观察,如果发现了类似的现象,说明它在给你发出需要重构的信号。
增加新功能的时候。此时,你发现既有的代码无法让你轻松添加新特性,需要花30%以上的时间做一些重复性的编码。
修bug的时候。此时,你发现排查逻辑非常困难,需要花费半小时甚至是数小时才能搞清楚代码在处理什么。
当从优秀的程序员口中说出觉得某段的代码写的太shit。(普通人的shit可能是到了无可救药的地步了,优秀的人觉得shit大概率还有救,否则他估计就离职不干了~)
/02 怎么重构?/
为了保证重构的质量,在你重构的过程中,一定要关注以下4个关键点:
始终工作。这其实也是《重构》中提到的理念,“「不改变软件可观察行为」的前提下”。如果你的重构需要大刀阔斧的进行,会导致很长一段时间软件无法运行起来,那么这就不是一个好的重构方式,因为在很长一段时间里你都不知道它会不会走向万劫不复的“深渊”。(有没有遇到过越重构越糟糕的经历?欢迎分享你的吐槽)
持续集成:很多人会将重构单独作为一个版本去做,其实这样会有一个新的问题,就是后续合版本是个痛苦的事情,而且容易出错。所以,应该就在平时的版本里去做重构,让每一次重构都可以跟着CI/CD持续进行。
随时中止:不管你的重构代码写到什么阶段,只要编译不报错就可以随时中止,立马切换到功能开发。只有达到这个标准,你的Leader才不会对重构又爱又恨。
过程可逆:假如我的重构出现了重大缺陷,它是可以快速回退的,而不需要从之前的版本当中去捞代码。毕竟,谁都无法100%保证每一次的重构都是perfect的。
可以回想一下,你之前做过的重构是否都符合了以上的这些要求?反正Z哥最近做的重构是不符合的,所以感觉很累很痛苦~
具体的重构工作其实说起来很简单,因为一段代码无非就是「输入参数」、「输出参数」、「方法体」3个东西,重构也自然以这几个地方展开。
/01 输入参数/
对于输入参数的重构,主要关注在参数的个数上。那些优秀的开源项目里,你几乎看不到参数很多的方法。
因为过多的参数个数,不但不容易理解,而且你在写调用这个方法的代码的时候也会很头疼,时不时要数一下这是第几个参数,对应的参数说明是什么。
有一些工具推荐的默认参数最大长度是7个(如SonarQube)。如果你没有更好的定义和理解,那么不妨以“7”这个标准来执行。
/02 输出参数/
输出参数只有一个,能够出乱子的空间也很小,所以一般来说不需要怎么优化。
唯一值得提醒的两点是:
参数类型尽量用强类型。弱类型的返回值虽然让你的Function向后兼容性很好,但是也带来了很多无法在编译期间被发现的问题。
不返回不需要的参数。添加更多参数在最初肯定是为了“跑在业务前面”,但这份好心往往最终带来的是更多“意料之外的耦合”,导致后续的重构成本大增。
/03 方法体/
对于方法体的重构是花费时间最多的地方,具体的方式方法也很多。但是我建议你一定要坚持一个核心要点,我将它称为「NRD重构法」,这3个字母分别表示:New、Replace、Delete。也就是说,做重构的时候不要直接在原来的方法体里改,重新建一个新的方法,然后等单测跑通之后再替换掉老方法,最后再把老方法删除。
只要做到这点,要满足前面提到的4个关键点,就没那么困难了。
具体的重构内容自然是以减少复杂度为核心思路去做。衡量代码复杂度有一个概念叫「圈复杂度」(也叫「循环复杂度」),在1976年由Thomas J. McCabe, Sr. 提出。现在有不少工具有统计这个指标。
复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。
复杂度大的代码往往伴随着大量的if/switch/for/foreach/try...catch/while等等。每一次试用都会让「圈复杂度」+1,并且其中的条件判断越多,增加的越快。
所以,常见的重构方式大多以降低代码的圈复杂度为主。比如,
将相同逻辑的代码抽离并封装到一处,可以避免在多个方法体里增加圈复杂度,只在一个方法体里增加。
通过AOP技术,不但可以将重复的代码剔除出当前方法体,还可以将try...catch之类的代码剔除出去,以降低复杂度。
通过一些语法糖或者框架,也可以降低复杂度。如,lambda表达式。
……
还有很多小众的重构技巧这里就不赘述了,真是觉得大家都应该读一读《重构》这本书。
多说一句,不提倡刻意降低代码行数的方法,因为你的复杂度不下降,减少代码行数只是“掩耳盗铃”而已。
另外,重构有一个最佳伴侣,就是单元测试。你想象一个画面,当你重构之前通过率100%的单元测试在重构完成后跑一遍,发现了10%的失败。此时你的心情肯定是“真香,否则一堆bug等着我修”。
不过,如果你的代码「圈复杂度」越高,单元测试写起来越费劲。如何写好单元测试可以看我之前写的文章《聊聊单元测试》。
最后,怎么判断重构的效果好不好呢?自然是工作效率是否提高了。
增加一个功能或者接口的时间是不是缩短了?
测试那边回归测试的平均时间是不是缩短了?
……
好了,就这么多。如果你还是觉得无从下手,不妨试试《重构》作者推荐的一种做法:
随机挑选一个目标,比如,“去掉一堆不必要的子类”。然后朝着目标前进,没把握就停下来。当你无法证明自己所做的修改能够保证原有程序的逻辑和语义时,立马停下来思考:当前做的重构是改善了?还是毫无成果需要撤销?
最后再次强力推荐《重构》这本书,里面有很多非常具体的代码重构方法,值得每一位程序员入手一本。
好了,总结一下。
这篇呢,Z哥和你分享了我对代码重构这件事的看法。要想提高你代码的“产出”,那么就得好好重视重构这件事。
在重构代码的「输入参数」、「输出参数」、「方法体」的时候需要持续保持以下4个关键点:
始终工作
持续集成
随时中止
过程可逆
这才能使得你的重构工作平稳的进行,而不会是一场赌博。
并且,重构方法体的时候要以降低「圈复杂度」为目的,而不是代码行数。如果条件允许,尽量多写一些单元测试来保障重构的稳定性。
希望对你有所启发。
重构可以使软件更容易地被修改和被理解,这个意义甚至大于所谓的“优化和改进”。Kent Beck大神曾也经说过:首先让代码架构易于改变,然后再进行简单的改进。
如果你想摆脱代码越改越痛苦的困境,那么赶紧行动起来吧。
推荐阅读:
做架构也得讲武德
聊聊单元测试
原创不易,如果你觉得这篇文章还不错,就「在看」或者「分享」一下吧。鼓励我的创作 :)
如果你有关于软件架构、分布式系统、产品、运营的困惑
可以试试点击「阅读原文」