前言
在开发代码的时候,经常觉得代码有一种坏味道,有一种迫切重构的想法,但是再一次的仔细阅读代码,却发现为了实现业务需求,代码就应该是现有这种模式的写法。之前也有在网上看一些代码重构的文章,但是直到看完《代码整洁之路》这本书,才算是有了一些明确代码重构的方法和思路。在这次对生产代码的一些重构实践中,对文章中提到并亲身体会过的部分小点,有了一些认识和体会,记录如下。
参考博客中的第一篇,重构十六字心法是下述小点的前提:旧的不变,新的创建,一步切换,旧的再见;
过长的函数及过长的函数参数列表
过长的函数参数列表导致的函数一定是会过长的,因为会存在参数的校验和参数的进一步加工处理,这些是可以将参数进行封装到数据结构以后提前进行处理的,同时也能大大减少函数调用长度,让阅读者更加清楚的聚焦函数内部逻辑处理。
过长的函数处理体是很难让人一下理清处理逻辑,它需要开发者一行一行的阅读完整个函数才能有大概的了解,它代表着函数内部的代码抽象层级不统一,这个时候需要我们对其进行分治,将一些聚合在一起的逻辑封装为一个函数,这样子我们一个大函数里面只有几个子函数调用,通过子函数名我们就能知道这个函数的大概处理逻辑。
过长的判断条件
当我们在if 判断条件中,使用 || &&连接的判断语句超过两个,我觉得就可以被认定为过长的判断条件。这种写法在代码阅读的时候,要求阅读者能够记住判断条件中使用的变量代表中的意义,有一定的记忆成本。另外这种过长的判断条件,还是有很大一部分的重复使用的情况,此时将它封装为一个单独函数,并起名一个有意义的名字,方便后续的阅读和维护。
变量的有效命名
变量的有效命名和函数的有效命名是一样的,一个可以望文生义的名字可以节省维护代码很大一部分时间,书中对此点进行了反复的强调。一个有意义的名字不是立即取出来的,当我们发现有更适合的命名时,一定要毫不犹豫的更改掉。
函数中的输出参数
类似于如下代码,函数在处理的时候,直接从params获取参数处理后并将结果put到Map中去。
void process(Map<String, Object> params);
在我们的生产代码中有很多这种的函数处理,当我去阅读的时候,给我带来很大的认知困扰。这种函数会某个代码片段中被调用一下,并没有返回结果,这个行为是非常容易迷惑人的,因为参数被自然的看做是函数的输入。如果该函数没有返回结果,对于这种的代码,我认为是可以随意调整的,但是随后又会存在一个函数依赖此函数的处理结果,而这些函数的命名或者参数中并没有体现出依赖关系,很容易出现严重的BUG。
我们应该极力避免输出参数的使用,即使是简单的将输入参数作为结果返回也比返回Void好很多。
函数名和函数实际行为的匹配,避免副作用
正如变量的有效命名所说,函数名必须有意义,但是函数行为也必须和函数名匹配,不能导致其它的副作用。check、is等这些开头的函数不应该对对象的状态进行更改。
垃圾的代码上增加注释
很多时候,我会在晦涩难懂的代码上增加注释以增强自己的理解,但往往下一次阅读的时候还是没有办法一目了然,仍然需要一行行从头阅读才能大概了解其意思。更加困难的一个事情,如果这个代码实现具备一定的业务背景,在交接的时候并没有提及的话,后来者很难阅读出其意图,更加别提去维护了。
如果代码实现的不好,不要增加注释了,重构它吧。
循规蹈矩的JavaDoc注释
如果是仅仅为了避免ide的提示,给函数增加上了循规蹈矩的Java doc注释,那还是干掉它吧。多余的注释只会分散开发者的注意力,另外不正确注释比不好的代码实现更加容易让人误解。没有注释的情况下,开发者使用的时候会查看实现,但是有注释的情况下,很可能会以注释内容为准,从而导致错误的行为。
统一变量命名概念
标识同一个概念意义的变量,命名一定要尽量统一。比如说分页的页码标识,pageNo、currentPage、pageNumber等混用情况。如果是其它更加复杂带有业务属性的变量,同一个概念使用不同的变量标识会带来更加严重的认知困难。
尽量避免返回null值
Java中的NullPointException是我们在编程希望极力避免掉的,因此我们在调用某个可能返回null的函数时,我们都需要增加判断条件,当返回值不为null才进行进一步的处理,所有函数调用初都不可避免的带上了这个判断条件处理,影响代码的阅读。
从另外一个角度来说,当函数提供者实现时,发现有调用方获取一个不存在的值,返回null也是一种可以理解的行为,在我的实现中就经常出现该类代码。之前我一直没有思考过不返回null,直接抛异常的处理方式,但这次去回看某些函数实现,正确的行为就应该是当结果为null时,抛出业务异常。
这一重构点我目前觉得大部分场景都难以避免,但是在后续实现业务逻辑的时候,遇上返回null的场景,一定要思考下抛出异常的处理逻辑,这种实现能够确保函数调用方一定能够拿到有值的结果,从而避免的重复、无味的非空判断。
火车头事件,代码的封装
当A依赖B,B依赖C时,如果A实现的一个逻辑处理,实际依赖于C的处理,我们有时会让A使用B拿到C以后,然后A直接调用C进行处理,这种行为被作者称为火车头事件。A直接调用C的行为,打破了B的封装,让A直接感受到了B的内部逻辑。
正确的行为应该是B封装C的行为,A只会感知到B,A调用B后,由B再来调用C。这个场景在我的代码中经常出现,当A、B、C都是自己编写的情况下,这个打破封装的行为自己是完全意识不到的,需要我们回过头来审视自己的代码,重新思考代码的正确行为。
重构时的单测
程序员都对自己的代码非常自信,特别是大部分的业务逻辑都是简单的增删改查的情况下,我自己单测是写得比较少的。但是错误往往出现于细微之处,特别在大部分正确的情况下,细小处的错误行为是很难寻找的。书中作者在讲述重构样例时,花了很大一番功夫来描述单测。如何让单测能够覆盖函数的所有行为是对程序员的一个考验,写好单测是重构的先决条件,不然往往会让自己事倍功半。
一定要有单测,TDD!
一定要有单测,TDD!
一定要有单测,TDD!
避免重复
Do not repeat youself! 这个原则被大部分的重构指引提及,而现代的ide能够智能识别到重复代码,另外当你发现你自己写了似曾相似的处理代码,那么你也在重复自己了,这种时候就应该提醒自己回头审视自己的代码了,一定是我们的处理逻辑存在问题才会导致这种的重复。当发现重复时,多回头看看!
参考博客
https://www.zhihu.com/question/19574943
https://martinfowler.com/bliki/BranchByAbstraction.html
http://insights.thoughtworkers.org/principles-of-refactoring/
https://www.infoq.cn/article/clean-code-refactor
http://insights.thoughtworkers.org/service-split-and-architecture-evolution/