一、代码竟会有“气味”
食物在腐烂之际,会散发出异味,提醒人食物已经坏掉了,需要处理。同样,如果代码中某处出现了问题,也会有一些症状。这些症状,被称之为“代码异味”(Code smell,也译作“代码味道”)。与食物腐败发出的味道不同的是,代码异味并非真正的气味,而是一种“暗示”,暗示我们的代码可能有问题,提示程序员需要对项目设计进行更进一步的查看。
代码异味一词最初是由Kent Beck在帮助Martin Fowler在编写《重构:改善既有代码的设计》一书时创造的。Martin Fowler对代码异味的定义是:代码异味是一种表象,它通常对应于系统中更深层次的问题。
代码异味的产生原因跟厨师的“清洗过程中故意保留”不一样,它更多地并非刻意为之,创造者也未必“品尝”过自己所写的代码,它更多地是由于设计缺陷或不良编码习惯而导致的不良代码症状。
这种异味也并非来自一种有据可查的标准,更多的是来自程序员的直觉。尤其是经验丰富和知识渊博的程序员,他们无需思考,只要通过查看代码或一段设计就可以立马对这个代码质量产生这种“感觉”,能对代码设计的优劣有一个大致的判断。这有点类似我们英语学到一定程度后,即便不能完全看懂文章,但凭借语感也能选出正确答案。
二、代码异味的影响
对于代码异味的出现我们其实无需过度紧张,因为在整个程序中代码异味是无处不在的。
一般情况下,有“异味”的代码也依旧能运行得很好。只是倘若重视不够,没有适当地维护或改进代码,代码质量就会下降,系统也会开始变得难以维护和扩展,同时也会增加技术债务。这就像做出有异味的九转大肠的的小胖厨师,在前期准备中对评委的建议置若罔闻,一意孤行,做出来的菜连自己都难以下咽。
所以团队应尽可能地做有质量的代码,减少甚至避免这些问题,产生高效益的成果。
三、如何辨别代码异味
代码是否存在代码异味,通常是靠程序员的主观判断,但由于语言、开发者、开发理论的不同,对代码异味的判断也会存在差异。
所以要想更精准地识别代码异味,获得更高的代码质量,程序员需要大量的实践和经验。不过,前辈们总结的经验也可以让我们少走一些弯路。Martin Fowler在《重构:改善既有代码的设计》一书中,列举了最常见的24种代码异味,可以帮助我们轻松识别,便于处理和改善它们:
1) 过大的类(Large Class)
一个类包含许多字段、方法或者代码行,并逐渐变得臃肿。
2) 数据泥团(Data Clumps)
代码的不同部分包含了相同的变量组,且这些数据总是绑在一起出现。
3) 过长参数列表(Long Parameter List)
指一个方法的参数超过了三个或四个。出现这种情况一般是将几种类型的算法合并到一个方法之后。
4) 基本类型偏执(Primitive Obsession)
创建一个原始字段比创建一个全新的类要容易得多,所以对于具有意义的业务概念如钱、坐标、范围等,很多程序员不愿意进行建模,而是使用基本数据类型进行表示,进而导致代码内聚性差、可读性差。
5) 神秘命名(Mysterious Name)
在编程中,命名是一件非常恼人的事情。一些可能只有自己看懂的命名,无疑加大了代码可读性的难度,有时甚至自己也会忘记这些命名的含义。
6) 重复代码(Duplicated Code)
这几乎是最常见的异味。当多个程序员同时处理同一程序的不同部分时,通常会发生这种情况。
7) 过长的函数(Long Function)
根据Martin Fowler的经验,通常活得最长、最好的程序,其中的函数都比较短。函数越长,就越难理解。
8) 全局数据(Global Data)
这是一个非常可怕且刺鼻的异味代码。因为从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底是哪段代码做出了修改。全局数据造成一次又一次的诡异Bug,让我们很难找出出错的代码。
9) 可变数据(Mutable Data)
如果可变数据的变量的作用域越大, 越容易出现问题。变量是可以更改的,但我们可能不知道是哪里改变了它。
10) 发散式变化(Divergent Change)
是指一个类受到多种变化的影响。
11) 霰弹式修改(Shotgun Surgery)
是指一种变化引发多个类相应修改。
12) 依恋情结(Feature Envy)
一个类使用另一个类的内部字段和方法的数据多于它自己的数据。
13) 重复的Switch(Repeated Switch)
在不同的地方反复使用switch逻辑。这带来的问题就是当我们想要增加一个选择分支时,就必须找到所有的switch,并逐一更新。
14) 循环语句(Loops)
在编程语言中,循环一直是程序设计的核心要素。在《重构》中,Martin Fowler认为它是一种代码异味,因为他们觉得如今的循环已经有点过时了。他们提出“以管道取代循环”,这样可以帮助我们更快看清被处理的元素以及处理它们的动作。
15) 冗赘的元素(Lazy Element)
这是几乎无用的组件。我们在设计代码时有时为了未来的功能设计出“预备”代码,但实际上从未实现;又或者这个类本来有用但随着重构,越来越小,最后只剩下一个函数。无论哪种,它们都是冗赘无用的。
16) 推测的通用性(Speculative Generality)
是指为了“以防万一”,支持预期的未来功能,但这些功能并未被实现,这些类、方法、字段或参数也从未被使用,结果导致代码变得难以理解和支持。
17) 临时字段(Temporary Field)
创建临时字段以用于需要大量输入的算法。但这些字段仅在算法中使用,其余时间不使用。
18) 过长的消息链(Message Chains)
当客户端请求另一个对象,该对象又请求另一个对象,依此类推时,就会出现过长的消息链。这些链意味着客户端依赖于类结构的导航。一旦发生更改,客户端也要跟着修改。
19) 中间人(Middle Man)
指一个类只执行一个动作,但将工作委托给另一个类,这种委托属于过度委托。该类也可能只是一个空壳,只负责委托且只有一件事。
20) 内幕交易(Insider Trading)
指模块之间大量地交换数据,增加模块之间的耦合。
21) 异曲同工的类(Alternative Classes with Different Interface)
是指两个类执行了相同的功能但具有不同的方法名称。
22) 纯数据类(Data Class)
指包含字段和访问它们的粗略方法(getter 和 setter)的类。这些只是其他类使用的数据容器。这些类不包含任何附加功能,并且不能独立操作它们拥有的数据。
23) 被拒绝的遗赠(Refused Bequest)
指如果子类复用了超类的行为,但又不愿意支持超类的接口的情况。
24) 注释(Comments)
程序员将其作为一种“除臭剂”使用情况下的行为。比如:一段代码有着长长的注释,但这段长注释的存在是因为代码很糟糕。
四、 如何对代码“除臭”
1)重构
上述代码异味没有优先级一说,所以对于程序员而言,只能依靠直觉和经验去决定是否需要重构。 重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。是实现敏捷性的最重要的技术因素之一。是程序员根据已识别出的气味然后将代码分成更小的部分的过程,再决定要么删除它们,要么用更好的代码替换它们,如此循环重复这个过程,直到异味消失,这样可能会提高代码质量并让代码变得更具简单性、灵活性和可理解性。
2)使用代码检测工具
识别和消除代码异味是一个令人厌烦且不确定的过程,而且也不可能手动查找到和删除掉所有异味,尤其是面对一个有着上千行异味的代码的时候。所以使用一些代码检测工具可以辅助我们进行快速大量地审查,帮助我们节约时间来做更为重要的工作,比如能专注于代码高层面的设计原则问题。
好了,关于代码异味的知识,算是讲了个清楚,那么让我们相约下一次代码评审吧!