1 神秘命名(Mysterious Name)
写下的代码应该直观明了。
整洁代码最重要的一环就是好的名字,所以我们会深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法。
改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。
为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。
2 重复代码(Duplicated Code)
如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。
如果重复代码只是相似而不是完全相同,请首先尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼。
如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间互相调用。
3 过长函数(Long Function)
据我们的经验,活得最长、最好的程序,其中的函数都比较短。更好的阐释力、更易于分享、更多的选择都是由小函数来支持的。函数越长,就越难理解。
不过说到底,让小函数易于理解的关键还是在于良好的命名。如果你能给函数起个好名字,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了些什么。
最终的效果是:你应该更积极地分解函数。
我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
我们如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常能指出码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。
4 过长参数列表(Long Parameter List)
过长的参数列身也经常令人迷惑。
5 全局数据(Global Data)
全局数据仍然是最刺鼻的坏味道之一。
全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。全局数据最显而易见的形式就是全局变量,但类变量和单例(singleton)也有这样的问题。
6 可变数据(Mutable Data)
对数据的修改经常导致出乎意料的结果和难以发现的bug。在一处更新数却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效。
7 发散式变化(Divergent Change)
如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。
8 霰弹式修改(Shotgun Surgery)
霰弹式修改类似于发散式变化,但又恰恰相反。
如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。
9 依恋情结(Feature Envy)
有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
当然,并非所有情况都这么简单。一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
有几个复杂精巧的模式破坏了这条规则,比如策略(Strategy)模式、访问者(Visitor)模式。使用这些模式是为了对抗发散式变化这一坏味道。
最根本的原则是:将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。
策略模式和和访问者模式使你得以轻松修改函数的行为,因为它们将少量需被覆写的行为隔离开来—当然也付出了“多一层间接性”的代价。
10 数据泥团(Data Clumps)
你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是一起出现的数据真应该拥有属于它们自己的对象。
首先请找出这些数据以字段形式出现的地方,一个好的评判办法是:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。
我们在这里提倡新建一个类,而不是简单的记录结构,因为一旦拥有新的类,你就有机会让程序散发出一种芳香。得到新的类以后,你就可以着手寻找“依恋情结”,可以帮你指出能够移至新类中的种种行为。
11 基本类型偏执(Primitive Obsession)
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
字符串是这种坏味道的最佳培养皿,比如,电话号码不只是一串字符。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。“用字符串来代表类似这样的数据”是如此常见的臭味,以至于人们给这类变量专门起了一个名字,叫它们“类字符串类型” (stringly typed)变量。
12 重复的switch (Repeated Switches)
在不同的地方反复使用同样的 switch 逻辑(可能是以switch/case 语句的形式,也可能是以连续的if/else 语句的形式)。
重复的 switch的问题在于:每当你想增加一个选择分支时,必须找到所有的switch,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。
13 循环语句(Loops)
管道操作(如 filter 和map)可以帮助我们更快地看清被处理的元素以及处理它们动作。
14 冗赘的元素(Lazy Element)
程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构。
可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类,根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许期望它将来有一天变大、变复杂,但那一天从未到来:也可能是因为,这个类原本是有用的,但随着重构的进行越变越小,最后只剩了一个函数。
15 夸夸其谈通用性(Speculative Generality)
当有人说“噢,我想我们有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要事情,这种坏味道就出现了。
这么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。
16 临时字段(Temporary Field)
有时你会看到这样的类:其内部某个字段仅为某种特定情况而设。
这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
17 过长的消息链(Message Chains)
如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。
在实际代码中你看到的可能是一长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
18 中间人(Middle Man)
对象的基本特征之一就是封装——对外部世界隐藏其内部细节。
封装往往伴随着委托。但是人们可能过度运用委托。
你也许会看到某个类的接口有一半的函数都委托给他类,这样就是过度运用。这时应该使用移除中间人,直接和真正负责的打交道。
19 内幕交易 (Insider Trading)
软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。
在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
20 过大的类(Large Class)
如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。
和“太多实例变量”一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。
最简单的解决方案是把多余的东西消弭于类内部。如果有5个“百行函数”,它们之中很多代码都相同,那么或许你可以把它们变成5个“十行函数”和10个提炼出来的“双行函数”。
观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的类。一旦识别出一个合适的功能子集,就使用提炼类、提炼超类或是以子类取代类型码将其拆分出来。
21异曲同工的类(Alternative Classes with Different Interfaces)
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。只有当两个类的接口一致时,才能做这种替换。
可以用改变函数声明将函数签名变得一致。但这往往还不够,请反复运用搬移函数将某些行为移入类中,直到两者的协议一致为止。
22 纯数据类(Data Class)
所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。
这样的类只是一种不会说话的数据容器,它们几乎一定被其他类分细琐地操控着。这些纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
23 被拒绝的遗赠(Refused Bequest)
如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,“被拒绝的遗赠”的坏味道就会变得很浓烈。
拒绝继承超类的实现,这一点我们不介意;但如果拒绝支持超类的接口,这就难以接受了。既然不愿意支持超类的接口,就不要虚情假意地糊弄继承体系,应该运用以委托取代子类或者以委托取代超类彻底划清界限。
24 注释(Comments)
我们之所以要在这里提到注释,是因为人们常把它当作“除臭剂”来使用。
常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。
注释可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚地说明了一切。
当你感觉需要撰写注释时,请尝试重构,试着让所有注释都变得多余。如果你不知道该做什么,这才是释的良好运用时机。除了用来记录未来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某事”,这类信息可以帮助将来的修改者。