文章目录
- 神秘命名(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)
- 冗赘的元素(Lazy Element)
- 夸夸其谈通用性(Speculative Generality)
- 临时字段(Temporary Field)
- 中间人(Middle Man)
- 内幕交易(Insider Trading)
- 过大的类(Large Class)
- 纯数据类(Data Class)
- 被拒绝的遗赠(Refused Bequest)
- 注释(Comments)
- 重构手法速查表
神秘命名(Mysterious Name)
顾名思义,当然,这里就包含了 变量,函数,类,模块等等元素的名称!
重复代码(Duplicated Code)
如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
如果重复代码只是相似而不是完全相同,请首先尝试用移动语句(将相关联的代码尽量放在在一起,便于之后重构)重组代码顺序,把相似的部分放在一起以便提炼。如果重复的代码段位于同一个父类的不同子类中,可以使用**函数上移(子类中大抵有两个类似的代码,可以考虑整合在父类中)**来避免在两个子类之间互相调用。
过长函数(Long Function)
关键不在于函数的长度,而在于函数“做什么”,逻辑上是不是能够拆分为一个单元。
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用提炼函数(106),最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常运用以查询取代临时变量 (
double basePrice = quantity * itemPrice;
//改为:
double basePrice() {
return quantity * itemPrice;
}
)来消除这些临时元素。 引入参数对象(将重复出现的参数整合为一个对象传入
)和**保持对象完整(在传参时,只需要某个对象的某些数据,多的时候就直接把该对象传入)**则可以将过长的参数列表变得更简洁一些。这里可以适时考虑一下 Effective Java 中的 builder 模式
如果你已经这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手锏——以命令取代函数(337) 待做
。
如何提炼出一个函数:
- 寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。
- 注意条件表达式和循环:对于表达式,你可以使用 简化条件表达式 (
)处理条件表达式。对于庞大的switch语句,其中的每个分支都应该通过 提炼函数(106)变成独立的函数调用。如果有多个switch语句基于同一个条件 进行分支选择,就应该使用以多态取代条件表达式(
)对于循环,请确定不要让循环内部做太多的事情, 合理的进行拆分!
过长参数列表(Long Parameter List)
- 如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用以查 询取代参数(324)去掉这第二个参数。
- 如果你发现自己正在从现有的数据结构 中抽出很多数据项,就可以考虑使用保持对象完(319)手法,直接传入原来 的数据结构。
- 如果有几项参数总是同时出现,可以用引入参数对象(140)将其 合并成一个对象。
- 如果某个参数被用作区分函数行为的标记(flag),可以使用 移除标记参数(这个我不是很认同, 还是视情况而定吧,我觉得!!!
) - 使用类可以有效地缩短参数列表。如果多个函数有同样的几个参数,引入一个类就尤为有意义。你可以使用
函数组合成类(待做)
,将这些共同的参数变成这个类的字段。
全局数据(Global Data)
封装并且保证其程序启动之后不可变!
可变数据(Mutable Data)
各种情况以及相应解决办法:
封装变量(Encapsulate Variable):private,get,set 函数等
。确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。- 如果一个变量在不同时候被用于存储不同的东西,可以使用
拆分变量
将其拆分为各自不同用途的变量,从而避免危险的更新操作。 - 使用
移动语句
和提炼函数
尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开。 - 设计API时,可以使用将
查询函数和修改函数分离
确保调用者不会调到有副作用的代码,除非他们真的需要更新数据。 - 对于后续不需要修改的对象数据,就不要为其提供
设值函数(就是设置值的函数)
。这样一来,该字段就只能在构造函数中赋值,我“不想 让它被修改”的意图会更加清晰,并且可以排除其值被修改的可能性——这种可能性往往是非常大的。
- 如果可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。 它不仅会造成困扰、bug和加班,而且毫无必要。消除这种坏味道的办法很简单,使用
以查询取代派生变量(待做)
即可。
- 如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题; 但随着变量作用域的扩展,风险也随之增大。可以用
函数组合成类(把相关性较强的一些函数创建一个类进行管理)
或者函数组合成变换(将修改逻辑收口,添加一个”增强版“的函数来统一封装修改)以便于寻找与debug
来限制需要对变量进行修改的代码量。 - 如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将
引用对象改为值对象
令其直接替换整个数据结构。如果传入的参数不需要再该函数中改变,那么就尽量不改变原始传入的参数,在这里就可以改为值对象或者加上 final/const 关键字进行限制。
发散式变化(Divergent Change)
如果需要更改,只用改某一个模块。如果还需要动到其他模块,那就不是一个较好的设计。“每次只关心一个上下文”这一点一直很重要,在如今这个信息爆炸、脑容量不够用的年代就愈发紧要。
各种情况以及相应解决办法:
- 如果发生变化的两个方向自然地形成了先后次序(比如说,先从数据库取出数据,再对其进行逻辑处理),就可以用
拆分阶段
将两者分开,两者之间通过一个清晰的数据结构进行沟通。 - 如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用
搬移函数
(频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。)
把处理逻辑分开。 - 如果函数内部混合了两类处理逻辑,应该先用
提炼函数
将其分开,然后再做搬移。如果模块是以类的形式定义的,就可以用提炼类
(一个类承担过多的责任会导致臃肿不堪,这个时候可以将相关的字段和函数从旧类搬移到新类。从而使一部分责任分离出去)来做拆分。
霰弹式修改(Shotgun Surgery)
霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。
各种情况以及相应解决办法:
- 使用
搬移函数
和搬移字段
(在程序中,某个字段被其所驻类之外的另一个类更多地用到。这个时候可以使用 Move Field(搬移字段) 在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。) 把所有需要修改的代码放进同一个模块里。
- 如果有很多函数都在操作相似的数据,可以使用
函数组合成类
。 - 如果有些函数的功能是转化或者充实数据结构,可以使用
函数组合成变换
。 - 如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上
拆分阶段
。 - 使用与内联(inline)相关的重构——如
内联函数
(将不必要的函数整合起来,不要分理该函数,刚好与提炼函数相反)或是内联类
(如果一个类不再承担足够责任,不再有单独存在的理由,就将其去掉,刚好与提炼类相反)
依恋情结(Feature Envy)
一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情形。
各种情况以及相应解决办法:
- 一个函数往往会用到几个模块的功能,那么它究竟该被置于何处呢?我们的原则是:先以
提炼函数
将这个函数分解为数个较小的函数并分别置放于不同地点,然后判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
数据泥团(Data Clumps)
将一些统一出现的、相同的几项数据(两个类中相同的字段、许多函数签名中相同的参数。)。使用提炼类
,引入参数对象
,保持对象完整
整合为一个对象代码瘦身。这么做的直接好处是可以将很多参数列表缩短,简化函数调用。
基本类型偏执(Primitive Obsession)
是指类型与数据没有达到一种优雅的契合。在该创建或者使用一个小对象(range类等)的时候,没有使用对象,而使用了基本类型。这时就可以使用以对象取代基本类型
来简化。
各种情况以及相应解决办法:
- 如果想要替换的数据值是控制条件行为的类型码,则可以运用以
子类取代类型码
加上以多态取代条件表达式
的组合将它换掉。 - 如果你有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该 运用
提炼类
和引入参数对象
来处理。
重复的switch (Repeated Switches)
关注重复的 switch:在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。解决办法就是:使用多态取代switch
冗赘的元素(Lazy Element)
如果外层包裹的壳不需要了就将其脱掉(可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类, 根本就是一个简单的函数)。解决办法是:内联函数
或是内联类
。如果这个类处于一个继承体系中,可以使用折叠继承体系
。
夸夸其谈通用性(Speculative Generality)
“噢,我想我 们总有一天需要做这事”的情况。过度拓展。
各种情况以及相应解决办法:
- 如果你的某个抽象类其实没有太大作用,请运用
折叠继承体系
、内联函数
和内联类
除掉。 - 如果函数的某些参数未被用上,可以用
改变函数声明
去掉这些参数。如果有并非真正需要、 只是为不知远在何处的将来而塞进去的参数,也应该用改变函数声明
去掉。 移除死代码,即毫无意义的代码
临时字段(Temporary Field)
**在类内部某个字段仅为某种特定情况而设。**这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。使用提炼类
以及搬移函数
把所有和这些字段相关的代码都放进这个新家。
中间人(Middle Man)
某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该使用移除中间人
,直接和真正负责的对象打交道。
### 各种情况以及相应解决办法:
如果这些中间人还有其他行为,可以运用以委托取代超类
或者以委托取代子类
把它变成真正的对象,这样你既 可以扩展原对象的行为,又不必负担那么多的委托动作。
内幕交易(Insider Trading)
**模块之间的数据产生大量交换。**这其实说明划分模块不优雅。这种情况的话就使用搬移函数
和 搬移字段
减少其数据交互。或者用隐藏委托关系
,
把另一个模块变成两者的中介。
过大的类(Large Class)
这种情况基本就是顾名思义。可以使用 提炼类
、提炼父类
、以子类取代类型码
手段去进行拆分成小而简的模块。
纯数据类(Data Class)
- 所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的 函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一 定被其他类过分细琐地操控着。这些类早期可能拥有public字段,若果真如此, 你应该在别人注意到它们之前,立刻运用
封装记录
将它们封装起来。对于那些不该被其他类修改的字段,请运用移除设值函数
- 找出这些取值/设值函数被其他类调用的地点。尝试以搬移函数 (198)把那些调用行为搬移到纯数据类里来。
- 例外情况就是,纯数据对象被用作函数调用的返回结果。
被拒绝的遗赠(Refused Bequest)
子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!
按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用函数下移
和字段下移
把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建 议:所有超类都应该是抽象(abstract)的。
注释(Comments)
- 废话注释
- 与代码逻辑不一致的注释
- 多余的注释(已经能够通过代码名称得出意思了,但是还添加的注释)
- 日志式注释(在每次编辑代码时,在模块开始处添加一条注释。这类注释就像是一种记录每次修改的日志。)
- 注释过多