一、代码坏味道
1、神秘命名
意义:
使函数、模块、变量和类,能清晰的表明自己的功能和用法(如果想不出一个好名字,说明背后可能潜藏着更深的设计问题)
常用重构手法:
(1)改变函数声明(124)
(2)变量改名(137)
(3)字段改名(244)
2、重复代码
意义:
如果要修改重复代码,你必须找出所有的副本来修改,如果在一个以上的地点看到相同代码结构,设法将它们合而为一
常用重构手法:
(1)“同一个类的两个函数含有相同表达式”可以提炼函数(106)提炼出重复代码
(2)如果重复代码只是相似而不是完全相同,先移动语句(223)重组代码顺序,把相似部分放在一起,以便提炼
(3)重复代码段位于同一个超类的不同子类,可以使用函数上移(350)
3、过长函数
意义:
过长函数:函数越长,越难理解
小函数:更好的阐释力、更易于共享(解耦)、更多的选择,易于理解(依赖好的命名),小函数可能会给阅读者带来一些负担,必须经常切换上下文。但现在编译器能很方便的再函数间跳转
常用重构手法
(1)遵循这样一条原则:每当感觉需要以注释说明点什么时,就把需要说明的东西写进一个独立函数中,并以其用途命名(命名的重要性)
(2)百分之99的场合,只需要提炼函数(106)
(3)如果函数内有大量参数和临时变量传递给新函数,会导致可读性降低,可以用以查询取代临时变量(178)消除临时变量
(4)引入参数对象(140)和保持对象完整性(319)将过长参数列表变得简洁
(5)如果仍然有太多临时变量和参数,可以用杀手锏--以命令取代函数(337)
(6)如何确定提炼哪一段代码:寻找注释
(7)条件表达式,可以运用分解条件表达式(260)
(8)对于庞大的switch语句,其中每个分支可以通过提炼函数(106),如果多个switch语句基于同一个条件进行分支选择,可以运用以多态取代条件表达式(272)
(9)循环,将循环和循环内的代码提炼到一个独立函数中,如果发现提炼出的循环很难命名,可能是其中做了几件不同的事,此时,可以使用拆分循环(227),将其拆分成各自独立的任务
4、过长参数列表
意义:过长的参数列表,会使得调用方比较迷惑,难以理解,增加调用者的难度
常用重构手法
(1)如果可以向某个参数发起查询而获取另一个参数,以查询取代参数(324),去掉第二个参数
(2)保持对象完整性(319)
(3)引入参数对象(140)
(4)如果某个参数被用作区分函数行为的标记flag,可以使用移除标记参数(314)
(5)如果多个函数有同样的几个参数,引入一个类就尤为有意义,可以使用函数组合成类(144)
5、全局数据
意义:全局数据在任何一个角落都可以修改它,且没有任何机制可以探测出到底哪段代码做出了修改
常用重构手法
(1)封装变量(132),把全局数据用一个函数包装起来,随后,将这个函数及其封装的数据搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域
6、可变数据
意义:我们在一处更新数据,却没有意识到在另一处期望着完全不一样的数据,引起bug
常用重构手法
(1)封装变量(132)确保更新操作通过很少几个函数进行,使其更容易监控和演进
(2)如果一个变量在不同时候被用于存储不同的东西,可以使用拆分变量(240)将其拆分为不同用途变量
(3)使用移动语句(223)和提炼函数(106)尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行更新操作代码分开
(4)设计API时,可以使用将查询函数和修改函数分离(306)确保调用者不会调用到有副作用的代码。
(5)移除设值函数(331)缩小变量作用域
(6)如果可变数据的值能在其他地方计算出来,这是一个特别刺鼻的坏味道。以查询取代派生变量(248)
(7)随着变量的作用域扩展,风险也增大,可以用函数组合成类(144)或函数组合成变换(149)来限制需要对变量进行修改的代码量
(8)如果一个变量在其内部结构中包含了数据,最好不要直接修改其中的数据,而是将引用对象改为值对象(252),令其直接替换整个数据结构
7、发散式变化
意义:我们希望软件能够更容易被修改,一旦需要修改,我们希望只在一处做修改。
发散式变化:如果某个模块经常因为不同的原因在不同的方向上发生变化(职责不单一),这就是发散式变化。比如如果新加入一个数据库,必须修改这3个函数,如果新出现一种金融工具,必须修改这4个函数,数据库和金融工具逻辑处理是两个不同的上下文。将它们搬移到各自独立模块中会更好(单一职责)
常用重构手法:
(1)如果两个方向自然形成了先后顺序,比如先从数据库取数据,再对金融逻辑进行处理,可以用拆分阶段(154)将两者分开
(2)如果两个方向之间有更多的来回调用,应该先创建适当模块,然后用搬移函数(198)把逻辑分开
(3)如果函数内部混合了两类处理逻辑,先提炼函数(106),再做搬移
(4)如果模块是以类的形式定义的,可以用提炼类(182)做拆分
8、散弹式修改
意义:每遇到某种变化,你都必须在许多不同的类中做出许多修改,需要修改的代码散布在四处,不但很难找到它们,也容易错过某个重要的修改
常用重构手法:
(1)使用搬移函数(198)和搬移字段(207)把所有需要修改的代码放进一个模块里
(2)如果很多函数都在操作相似的数据,可以使用函数组合成类(144)
(3)如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,常常用拆分阶段(154)
(4)面对散弹式修改,常用的策略就是使用与内联相关的重构,如内联函数(115)或内联类(186)将不该分散的逻辑拽回一处
9、依恋情结
含义:所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。当一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流
常用重构手法:
(1)这个函数想跟这些数据待在一起,就使用搬移函数(198)
(2)有时,函数中只有一部分受依恋之苦,可以使用提炼函数(106)再搬移函数(198)带它去合适的地方
10、数据泥团
含义:常常可以看到相同的三四项数据,两个类中相同的字段、许多函数签名中相同参数。这些总是绑在一起出现的数据应该拥有属于自己的对象。
意义:可以将参数列表缩短,简化函数调用
常用重构手段:
(1)首先找出这些数据以字段形式出现的地方,运用提炼类(182)将它们提炼到一个独立对象中
(2)然后将注意力转移到函数签名上,运用引入参数对象(140)或保持对象完整性(319)为它瘦身
11、基本类型偏执
含义:很多时候我们不愿意创建对自己问题域有用的基本类型,如钱、坐标、范围等。于是我们把钱当做普通数字来处理的情况,计算物理量时无视单位(如把英寸与毫米相加)的情况以及大量if(a < upper && lower)这样的代码。制造重复代码和使用成本
常用重构手法:
(1)以对象取代基本类型(174)将原本单独存在的数据值替换为对象
(2)如果要替换的数据值是控制条件行为的类型,可以运用以子类取代类型码(362)加上以多态取代条件表达式(272)的组合将其替换掉
(3)如果有一组总是同时出现的基本类型数据,这就是数据泥团征兆,运用提炼类(182)和引入参数对象(140)来处理
12、重复的switch
重复的switch问题在于:每当你想增加一个分支时,必须找到所有的switch,并逐一更新,可以使用多态
13、循环语句
循环优点过时,如今函数作为一等公民,得到广泛支持,可以使用以管道取代循环(231),管道操作(如filter和map)可以帮助我们更快的看清被处理的元素以及处理它们的动作(即提高可读性)
14、冗余的元素
程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕提供更好的名字也好,但有时我们真的不需要这层结构。
常用重构手法:
(1)可能有这样一个函数,它的名字跟代码实现看起来一模一样,可以使用内联函数(115)
(2)可能有这样一个类,根本就是一个简单的函数,可以使用内联类(186),如果这个类处于一个继承体系中,可以使用折叠继承体系(380)
15、夸夸其谈通用性
含义:当有人说“我想总有一天需要做这事”,并因而企图用各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。
意义:这么做的结果往往造成系统更难理解和维护
常用重构手法
(1)如果某个抽象类其实没有太大作用,请运用折叠继承体系(380)
(2)不必要的委托可运用内联函数(115)和内联类(186)去除
(3)如果函数的某些参数未被用上,或者只是为了不知远在何处的将来而塞进去的参数,可以用改变函数声明(124)去掉
(4)如果函数或类的唯一用户是测试用例,这就飘出了坏味道,可以先删掉测试用例,再移除死代码(237)
16、临时字段
意义:有时你会看到这样的类,其内部某个字段仅为某种特定情况而设,这样的代码不易理解,因为你通常认为对象在所有时候都需要它的所有字段。
常用重构方法:
(1)使用提炼类(182)给这个可怜的孤儿创造一个家,然后用搬移函数(198)把所有和这些字段相关的代码都放进这个新类中。
(2)也许你还可以使用引入特例(289)在“变量不合法”的情况下创建一个替代对象,从而避免写出条件式代码
17、过长的消息链
含义:如果看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,这就是消息链
意义:采取这种方式,意味客户端将与查找过长中的导航结构紧密耦合。一旦对象间关系发生变化,客户端就不得不做出修改
常用重构方法:
(1)隐藏委托关系(189),可以在消息链的不同位置采用这种重构手法。
18、中间人
(1)当某个类的接口有一半的函数都委托给其他类,这样就是过度运用。使用移除中间人(192),直接和真正负责的对象打交道。
(2)如果这样的函数只有少数几个,可以运用内联函数(115)把它们放进调用端。
19、内幕交易
意义:开发者及其反感在模块间大量交换数据,因为会增加模块间耦合,实际情况,一定的数据交换不可避免
常用重构方法:
(1)如果两个模块总是交流频繁,就应该搬移函数(198)和搬移字段(207)减少私下交流
(2)如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些共用数据放在管理良好的地方;或者隐藏委托关系(189)把另一个模块变成两者的中介
(3)继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望,请以委托取代子类(381)或以委托取代超类(399)让其离开继承体系
20、过大的类
意义:如果想利用单个类做太多事情,其内往往会出现太多字段,一旦如此,重复代码就会接踵而至;和"太多实例变量"一样,类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头
常用重构手法
(1)运用提炼类(182)将几个变量一起提炼至新类中,如果新类适合作为一个子类,往往提炼超类(375)或者以子类取代类型码(361)其实就是提炼子类往往比较简单
(2)观察一个大类的使用者,看看使用者是否只用到这个类所有功能的一个子集,每个子集都能拆分成独立的类,试用提炼类(182)、提炼超类(375)、或是以子类取代类型码(362)将其拆分出来
21、异曲同工的类
意义:使用类的好处之一就在于可以替换:今天用这个类,未来可以替换成另一个类(方便修改和扩展),但只有当两个类的接口一致时,才能做这种替换
常用重构手法
(1)可以用改变函数声明(124)将函数签名变得一致,但这往往不够,请反复运用搬移函数(198)将某些行为移入类中,直到两者协议一致为止
(2)如果搬移过程造成了重复代码,或许可以运用提炼超类(375)补偿下
22、纯数据类
含义:一个类它们拥有一些字段,以及用于访问这些字段的函数,除此之外一无长物,它们几乎一定被其他类过分细锁的操控着;
意义:纯数据类常常意味着行为被放错了地方,也就是说,只要把处理数据的行为从客户端搬移到纯数据类里,就能大为改观
常用重构手法:
(1)这些类早期可能拥有public字段,应该在别人没有注意它之前使用封装记录(162)。
(2)对于哪些不该被其他类修改的字段,运用移除设值函数(331)
(3)找出取值/设值函数的调用点,尝试以搬移函数(198)把那些调用行为搬移到纯数据类中。
(4)也有例外,比如作为函数调用的返回结果
23、被拒绝的遗赠
含义:超类只持有所有子类共享的东西,如果某些子类只挑选了几个字段和函数,按传统说法,就是继承体系设计错误
常用重构手法:
(1)运用函数下移(359)和字段下移(361)把所有用不到的函数推给子类(这种坏味道很淡,通常不值得理睬)
(2)如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,这种坏味道比较强烈,就不要虚情假意的糊弄继承体系,运用以委托取代子类(381)或者以委托取代超类(399)
24、注释
(1)如果你需要注释来解释一段代码,试试提炼函数(106)
(2)如果函数已经提炼,但还是需要注释解释其行为,试试改变函数声明(124)为它改名
(3)如果需要注释说明某些规格,试试引入断言(302)