何时必须重构是没有一个精确衡量的标准的。没有任何量度规矩比得上见识广博的直觉。你必须培养自己的判断力,学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码才算太长。本章我们举例一些“坏味道条款”,以作参考。
1. 神秘命名(Mysterious Name)
整洁代码最重要的一环就是好的名字,好的命名能清晰地表达自己的功能和用法。改名可能是最常用的重构手法,包括改变函数声明、变量改名、字段改名。很多人经常不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。
2. 重复代码(Duplicated Code)
相同的代码结构,设法将它合二为一,程序会变得更好。一旦有重复代码,你就得加倍仔细,留意其间细微的差距。如果要修改重复代码,你必须找出所有的副本来修改。
- 最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这是你可以采用提炼函数提炼出重复代码,然后让两个地点都调用提炼出来的函数。
- 如果重复代码只是相似而不是完全相同,可以首先尝试用移动语句重组代码顺序,把相似的部分加以提炼。
- 如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间相互调用。
3. 过长函数(Long Function)
一般活得最长、最好的程序,其中函数都比较短。小函数间接性带来的好处——更好的阐释力、更易于分享、更多的选择。
函数越长,就越难理解。我们遵循这样一个原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
- 函数变短——提炼函数
- 函数内的大量参数和临时变量——以查询取代临时变量
- switch语句——每个分支都应该提炼函数变成独立的函数调用。如果有多个switch基于同一个条件进行分支选择,应该使用以多态取代条件表达式
- 循环——将循环和循环内的代码提炼到一个独立的函数中。如果一个函数中做了几件不同的事,应该使用拆分循环将其拆分成各自独立的任务
4. 过长参数列表(Long Parameter List)
- 以查询取代参数
- 几项参数总是同时出现——引入参数对象将其合并成一个对象
- 使用类可以有效地缩短参数列表——使用函数组合成类,将这些共同的参数变成这个类的字段
5. 全局数据(Global Data)
全局数据也是最刺鼻的坏味道之一。全局数据的问题在于,从代码库的任何一个角落都可以修改它,而没有任何机制可以探测出到底哪段代码做出了修改。
首要的防御手段是封装变量。把全局数据用一个函数包装起来,尽量控制其作用域,至少可以看见修改它的地方,并开始控制对它的访问。
良药与毒药的区别在于剂量。有少量的全局数据或许无妨,但数量越多,处理的难度就会指数上升。即便只有少量的数据,我们也愿意将它封装起来,这是在软件演进过程中对应变化的关键所在。
6. 可变数据(Mutable Data)
- 确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进——封装变量
- 变量在不同时候被用于存储不同的东西——拆分变量将其拆分为各自不同用途的变量
- 尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开——移动语句和提炼函数
- 可变数据的值在其他地方能计算出来,会造成困扰、bug和加班——以查询取代派生变量
- 限制需要对变量进行修改的代码量——函数组合成类或函数组合成变换
- 如果一个变量在其内部结构中包含了数据,不要直接修改其中的数据——将引用对象改为值对象
7. 发散式变化(Divergent Change)
如果某个模块经常因为不同的原因在不同的地方上发生变化,发散式变化就出现了。“每次只关心一个上下文”这一点一直很重要。
通常我们需要通过拆分阶段、搬移函数、提炼函数、提炼类等方式来处理。
8. 散弹式修改(Shotgun Surgery)
散弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,都必须在许多不同的类中做出许多小修改,就是散弹式修改。如果需要修改的代码散布四处,就很容易错过某个重要的修改
- 把所有需要修改的代码放进同一个模块中——搬移函数和搬移字段
- 把本不该分散的逻辑拽回一处——内联函数或者内联类
9. 依恋情结(Feature Envy)
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后把这个函数和那些数据摆在一起(将总是一起变化的东西放到一块儿)
10. 数据泥团(Data Clumps)
一些地方的相同数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于自己的对象。我们可以运用提炼类将它们提炼到一个独立对象中,然后将注意力转移到函数签名上,运用引入参数对象或保持对象完整为它瘦身。
11. 基本类型偏执(Primitive Obsession)
大多数编程环境都大量使用基本类型,即整数、浮点数和字符串等。很多程序员不愿意创建对自己的问题域有用的基本类型,如钱、坐标、范围等。
字符串是这种坏味道的最佳培养皿。比如电话号码不只是一字符串。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。
你可以运用以对象取代基本类型将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。
12. 重复的switch(Repeated Switches)
任何switch语句都应该用以多态取代条件表达式消除掉。甚至有人听过这样的观点:所有条件逻辑都应该用多态取代,绝大多数if语句都应该被扫进历史的垃圾桶。
但如今并不是所有的switch语句都被禁止使用,我们更关注重复的switch:在不同的地方反复使用同样的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)
如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。和“太多实例变量”一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。最简单的解决方案是把多余的东西消弭于类内部。观察一个大类的使用者,经常能找到如何拆分类的线索。
21. 异曲同工的类(Alternative Classes with Different Interfaces)
使用类的好处之一就在于可以替换:今天用这个类,未来可以换成另外一个。但只有当两个类的接口一致时,才能做这种替换。可以用改变函数声明将函数签名变得一致。但这往往不够,还需要反复使用搬移函数将某些行为移入类中,知道两者的协议一致为止。
22. 纯数据类(Data Class)
纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。
找出这些取值/设值函数被其他类调用的地方。尝试以搬移函数把那些调用行为搬移到纯数据类里来。如果无法搬移整个函数,就运用提炼函数产生一个可被搬移的函数。
纯数据类常常意味着行为被放在了错误的地方。只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
23. 被拒绝的遗赠(Refused Bequest)
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办?这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用函数下移和字段下移把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。
但是如果子类复用了超类的行为,却又不愿意支持超类的接口,“被拒绝的遗赠”的坏味道就会变得浓烈。既然不愿意支持超类的接口,就不要虚情假意地糊弄继承体系,应该运用以委托取代子类或者以委托取代超类彻底划清界限。
24. 注释(Comments)
注释并不是不该写。注释可以带我们找到本章先前提到的各种坏味道。运用各种重构手法把坏味道去除之后,我们发现:注释已经变得多余了,因为代码已经清楚地说明了一切。
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
下一章:构筑测试体系