知道"如何"不代表知道"何时",决定何时重构及何时停止和知道重构机制如何运转一样重要!
Kent Beck提出了用味道来形容重构的时机。
作者并不试图制定一个何时必须重构的精确衡量标准。从经验来看,没有任何量度规矩比得上见识广博者的直觉。对于开发者,必须培养自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少行代码才算太长。
坏味道条款
- 神秘命名(Mysterious Name)
- 重复代码(Duplicated Code)
- 过长函数(Long Function)
- 过长参数列表(Long Parameter List)
- 全局数据(Global Data)
- 可变数据(Mutable Data)
- 发散式变化(Divergent Change)
- 散弹式修改(Shotgun Surgery)
- 依恋情结(Feature Envy)
- 数据泥团(Data Clumps)
- 基本类型偏执(Primitive Obsession)
- 重复的switch(Repeated Switches)
- 循环语句(Loops)
- 冗赘的元素(Lazy Element)
- 夸夸奇谈通用性(Speculative Generality)
- 临时字段(Temporary Field)
- 过长的消息链(Message Chains)
- 中间人(Middle Man)
- 内幕交易(Insider Trading)
- 过大的类(Large Class)
- 异曲同工的类(Alternative Classes with Different Interfaces)
- 纯数据类(Data Class)
- 被拒绝的遗赠(Refused Bequest)
- 注释(Comments)
神秘命名
整洁代码最重要的一环是好的名字,所以需要深思熟虑如何给函数、模块、变量和类命名,使它们能清晰地表明自己的功能和用法。
命名使编程中最难的两件事之一。因此改名可能是最常用的重构手法,包括改变函数声明(124)(用于给函数改名)、变量改名(137)、字段改名(244)等。
重复代码
一旦有重复代码的存在,阅读这些重复的代码时就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,必须找出所有的副本来修改。
最单纯的重复代码就是"同一个类的两个函数含有相同的表达式"。通过提炼函数(106)、或前后移动语句(223)等方法来提炼重复的代码。
过长函数
每当感觉需要以注释来说明点什么的时候,就可以把需要说明的东西写进一个独立函数中,并以其用途命名。
如果函数内有大量的参数和临时变量,可能会对函数的提炼形成阻碍,或者在提炼出来的函数参数中并没有带来可读性的改善。这里可以运用以查询取代临时变量(178)来消除这些临时变量。引入参数对象(140)和保持对象完整(319)则可以将过长的参数列表变得更简洁一些。
如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。哪怕是只有一行代码,如果需要以注释说明,那也值得将它提炼到独立函数中。
对于庞大的switch语句,其中的每个分支都应该通过提炼函数编程独立的函数调用。如果有多个switch语句基于同一个条件进行分支选择,就应该使用以多态取代条件表达式。
至于循环,可以将循环和循环体内的代码提炼到一个独立的函数中。
过长参数列表
过长的参数列表会让人产生迷惑。处理这种问题方式很多:
- 如果可以向某个参数发起查询而获得另一个参数的值,那么可以直接使用**以查询取代参数(324)**去掉第二个参数
- 如果是从现有的数据结构中选取出很多项,可以使用**保持对象完整(319)**手法,直接传入原来的数据结构
- 如果几项参数同时出现,可以用**引入参数对象(140)**将其合并成一个对象
- 同样使用**函数组合成类(144)**也可以很有效地缩短参数列表
全局数据
全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。
首要的防御手段是封装变量(132)。把全局数据用一个函数包装起来,至少可以看到修改它的地方,并开始控制对它的访问。之后可以把它放在一个类或者模块中,只允许模块内的代码使用它,尽量控制作用域。
可变数据
对数据的修改经常导致出乎意料的结果和难以发现的bug。需要采用一些手段来约束对数据的更新,降低风险。
- 可以用**封装变量(132)**确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进
- 如果变量在不同时候被用于存储不同的东西,可以使用**拆分变量(240)**将其拆分为各自不同用途的变量
- 把能够通过查询计算出来的变量**以查询取代派生变量(248)**方法处理
- 使用**函数组合成类(144)或者函数组合成变换(149)**来限制需要对变量进行修改的代码量
- 如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用**将引用对象改为值对象(252)**令其直接替换整个数据结构
发散式变化
一旦需要修改,只需要跳到系统的某一点,只在该处做修改。这是必须要做到的抽象,若因为修改一处代码同时牵连要修改多个函数,就会带来很严重的坏味道。
- 如果发生变化的两个方向自然地形成了先后次序,可以用**拆分阶段(154)**将两者分开,两者通过一个清晰的数据结构进行沟通
散弹式修改
散弹式修改类似于发散式变化,如果遇到某种变化,都必须在许多不同的类内做出许多小修改,那么面临的坏味道就是散弹式修改。
面对这样的问题,一个常用的策略就是使用与内联(inline)相关的重构—如内联函数(115)或是内联类(186)—把本不该分散的逻辑拽回一处。完成内联之后,可能会闻到过长函数或者过大的类的味道,不过总可以用于提炼相关的重构手法将其拆解成更合理的小块。
依恋情结
有时会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。这种情况可以将这个函数跟这些数据待在一起,使用**搬移函数(198)把它移过去。如果函数只是一部分受影响,那可以先用提炼函数(106)把这一部分提炼到独立的函数中,再使用搬移函数(198)**移过去。
如果一个函数在几个模块中都用到,原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
数据泥团
往往在开发中,很多地方会看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。对于这些数据应该有属于它们自己的对象。可以采用**提炼类(182)将它们提炼到一个独立对象中,然后将注意力转移到函数签名上,运用引入参数对象(140)或保持对象完整(319)**为它瘦身。
有用的类被创建出阿里,大量的重复被消除,后续开发得以加速,原来的数据泥团就会在其范围能发挥更大的价值。
基本类型偏执
重复的switch
任何switch语句都应该用**以多态取代条件表达式(272)**消除掉。甚至所有条件逻辑都应该用多态取代。
我们关注的是重复的switch:在不同的地方反复使用同样的switch逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形式)。重复的switch的问题在于:每当想增加一个选择分支时,必须找到所有的switch,并逐一更新。
循环语句
可以用管道取代循环(231)。管道操作(如filter和map)可以帮助更快地看清被处理的元素以及处理它们的动作。
冗赘的元素
有时程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕只是提供更好地名字,但有时是不需要这层额外的结构。看起来略显累赘。
通常只需使用内联函数(115)或内联类(186)。如果类处于一个继承体系中,可以使用折叠继承体系(380)。
夸夸奇谈通用性
存在一些方法或参数是用于在未来某一天会实现的,但暂时还未实现就写了进去,这会加深对系统的理解和维护。应该搬移掉。
如果抽象类没有太大作用,运用折叠继承体系(380)。不必须要的委托可运用**内联函数(115)和内联类(186)除掉。如果函数的某些参数未被用上,可以用改变函数声明(124)去掉这些参数。如果有并非真正需要、只是为不知远在何处的将来而塞进去的参数,应该用改变函数声明(124)**去掉。
临时字段
有些类:其内部某个字段仅为某种特定情况而设,违背了通常认为对象在所有时候都需要它的所有字段的思维。
使用**提炼类(182)和搬移函数(198)**把所有和这些字段相关的代码都放到单独的类中统一管理。
过长的消息链
用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后接着请求另一个对象…这就是消息链。容易造成的问题很明显,客户端代码将与查找过程中的导航结构紧密耦合。如果对象间的关系发生变化,客户端就必须做出修改。
使用隐藏委托关系(189)。先观察消息链最终得到的对象是用来干什么的,看能否**提炼函数(106)把使用该对象的代码提炼到一个独立的函数中,在运用搬移函数(198)**把这个函数推入消息链。
中间人
对象的基本特征之一就是封装–对外部世界隐藏其内部细节。封装往往伴随着委托。而过度运用委托,如某个类的接口有一半的函数都委托给其他类。
使用移除中间人(192),直接和真正负责的对象打交道。如果这些函数比较少,可以运用**内联函数(399)**把它们放进调用端。
内幕交易
模块之间的数据交换会增加模块间的耦合。在实际情况里,一定的数据交换是不可避免的,但必须尽量减少这种情况,把交换放到明面上。
如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些公用的数据放在一个管理良好的地方;或者用隐藏委托关系(189),把另一个模块变成两者的中介。
放大的类
单个类做太多事情,往往会出现太多字段。重复代码也就不可避免。
可以运用提炼类(182)将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。也可以观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只用到这个类的所有功能的一个子类,每个这样的子类都可能拆分成一个独立的类。一旦识别出一个合适的功能子集,就试用提炼类(182)、**提炼超类(375)或者以子类取代类型码(362)**将其拆分出来。
异曲同工的类
使用类的好处之一就是可以替换,但替换两个类必须保证接口一致性,才能替换。
可以通过**改变函数声明(124)将函数签名变得一致,必须保证两者之间的所有都一致才能替换。如果搬移过程造成了重复代码,可运用提炼超类(375)**补偿。
纯数据类
被拒绝的遗赠
子类应该继承超类的函数和数据。但如果继承的函数和数据有子类不需要的,意味着继承体系设计错误。
可以为这个子类新建一个兄弟类,再运用**函数下移(359)和字段下移(361)**把所有用不到的函数下推给那个兄弟。这样超类就只持有所有子类共享的东西。
注释
注释常常被用于错误的使用,包括因为代码设计的很糟糕,才备注了常常的注释。借助注释可以找到本章先前提到的各种坏味道。找到会味道之后,首先应该以各种重构手法把坏味道去除。完成之后会发现:注释已经变得多余了,因为代码已经清楚的说明了一切。
只有在不知道该做什么,才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记并无十足把握的区域。可以记述下自己"为什么这么做"。帮助将来的修改者。