重构-坏的味道

作者: shawn_sundy | 来源:发表于2019-12-01 16:35 被阅读0次

Duplicated Code(重复代码)

场景与处理

同一个类的两个函数含有相同的表达式

Extract Method(提炼方法)提炼出重复的代码,然后让两个地方点都调用被提炼出来的那一段代码

两个互为兄弟的子类

内含相同表达式

对两个类都是用Extract Method,然后再对被提炼出来的代码使用Pull Up Method(函数上移),将它推入超类

代码类似,并非完全相同

Extract Method将相似部分和差异部分割开,构成单独一个函数,然后运用Form Template Method(塑造模板函数)获得一个Template Method设计模式

有些函数以不同的算法做相同的事情

选择其中比较清晰的一个,并使用Substitue Algorithm(替换算法)将其他函数的算法替换掉

两个毫无相关的类出现Duplicated Code

1.对其中一个是用Extract Class(提炼类),将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类

2.如重复代码所在的函数的确只属于某个类,另一个类只能调用它,抑或是这个函数可能属于第三个类。而另外两个类应该引用第三个类。必须觉得这个函数放在合适的位置。


Long Method(过长函数)

“间接层”所能带来的全部利益——解释能力、共享能力、选择能力)全都由小型函数支持的

原则

每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现首发)命名

场景与处理

99%的场景

使用Extract Method

大量参数和临时变量

使用Replace Temp With Query(以查询取代临时变量)来消除临时元素;Introduce Parameter Object(引入参数对象)和Preserve Whole Object(保持对像完整)则可以将过长的参数列表变得更简洁一些;如果仍有很多参数,则使用Replace Method with Method Object(以函数对象取代函数)

条件表达式和循环

使用Decompose Conditional(分解条件表达式)处理条件表达式。至于循环,则将循环和其内的代码提炼到一个独立函数中


Large Class(过大的类)

场景与处理

单个类做太多事情,其内往往出现太多实例变量

可以运用Extract Class(提炼方法)将几个变量一起提炼至新类内。提炼时选择类内彼此相关的变量到某个组件内,比如相同的前缀或字尾;如果组件适合作为子类,则使用Extract Subclass(提炼子类)比较简单。

类内如果有太多代码

往往使用Extract Class和Extract Subclass。如果确定客户端如何使用它们,就用Extract Interface(提取接口)为每一种使用方法提炼出一个接口。

GUI类

可能需要把数据和行为迁移到一个独立的领域对象中。另外可能两边需要各保留一些重复数据,并保持两边同步。Duplicate Observed Data(复制“被监视数据”)很有用。


Long Parameter List(过长的参数列)

太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。

处理

类内部调用,向函数所属类内的一个字段或另一个参数发出请求就可以取代一个传参,则使用Replace Parameter with Method(以函数取代参数)。

如参数很多都是从一个对象中取出的,可以使用Preserve Whole Object(保持对象完整)将来自同一对象的一堆数据收集起来,并以对象替换它们作为传参。

如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object(引入参数对象)为它们制造一个“参数对象”替换传参

例外

有时候你明显不希望造成“被调用对象”与“较大对象”间的某种依赖关系。这时候将数据从对象中拆解出来单独作为参数,也很合情合理。


Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上发生变化,那么Divergent Change就出现了。

一个类受多种变化影响

处理

针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内所有内容都应该反应此变化

应该找出某特定原因而造成的所有变化,然后运用Extract Class将它们提炼到另一个类中。


Shotgun Surgery(散弹式修改)

Shotgun Surgery类似Divergent Change,但恰恰相反,如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,这就是Shotgun Surgery

一种变化引发多个类相应修改

处理

使用Move Method(搬移函数)和Move Field(搬移字段)把所有需要修改的代码放进同一个类。如果没有合适的类可以安置这些代码,就创造一个。

通常可以使用Inline Class(内联化类)把一系列相关行为放在同一个类。


Feature Envy(依恋情结)

场景与处理

函数对某个类对兴趣搞过对自己所处类的兴趣,通常焦点便是数据

把这个函数移至另一个地方。应该使用Move Method把它移到它该去的地方;有时函数中只有部分受这种依恋之苦,这时应该先使用Extract Method把这一部分提炼到独立函数中,再使用Move Method带它去它的梦中花园。

一个函数往往会用到几个类的功能

判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。也可以先以Extract Method将这个函数分解为数个较小函数并分别置放于不同地点。

例外

Strategy和Visitor模式就破坏这个规则。不过使用这些模式是为了对抗Divergent Change:将总是一起变化的东西放在一块儿。


Data Clumps(数据泥团)

如果删除众多数据中的一项,这么做其他数据因而失去了意义,则就应该为它们产生一个新的对象。

场景与处理

两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象

首先找出这些数据以字段形式出现的地方,运用Extract Class将它们提炼到一个独立对象中,然后将注意力转移到函数签名上,运用Introduce Parameter Object(引入参数对象)或Preserve Whole Object(保持对象完整)为它减肥。


Primitive Obsession(基本类型偏执)

结构类型允许你将数据组织成有意义的形式;基本类型则构成结构类型的积木块。

对象打破了横亘于基本数据和提及较大的类之间的界限。所以可以编写一些与基本类型无异的小型类,替换数值

处理

单独存在的数据值替换称为对象

运用Replace Data Value with Object(以对象取代数据值 )

如果想要替换的数据值是类型码,而它并不影响行为

可以运用Replace Type Code with Class(以类取代类型码)。

有与类型码相关的条件表达式

运用Replace Type Code with Subclass(以子类取代类型码)或Replace Type Code with State/Strategy(以State/Strategy 模式取代类型码)

如果有一组应该总被放在一起的字段,可运用Extract Class。

在参数列中看到基本型数据,可用Introduce Parameter Object。

如正从数组中挑选数据,可用Replace Array with Object(以对象取代数组)

Ps:类型码即被提取出来的魔法值等常量


Switch Statements(switch语句)

少用switch

面对对象程序的一个最明显的特征就是少用switch(或case)语句

switch语句的问题在于重复,switch语句散布于不同地点,如要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。面对对象中的多态概念可以有很好的解决办法。

场景与处理

switch语句常常根据类型码进行选择

应使用Extract Method将switch语句提炼到一个独立函数中,再以Move Method将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦完成继承结构厚,就可以用Replace Conditional with Polymorphism(以多态取代条件表达式)。

只是在单一函数中有些选择事例,且并不想改动它们

可以使用Replace Parameter with Explicit Methods(以明确函数取代参数)。如果选择条件之一是null,则可以试试Introduce Null Object(引入Null对象)


Parallel Inheritance Hierarchies(平行继承体系)

场景与处理

它是Shotgun Surgery的特殊情况:如果每当为某个类增加一个子类,必须也为另一个类相应增加资格子类;如果发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同。

让一个继承体系的事例引用另一个继承体系的实例。如果再接再厉运用Move Method和Move Field,就可以将引用端端继承体系消弭于无形


Lazy Class(冗赘类)

如果一个类的所得不值其身价,它就应该消失

场景与处理

如果某些子类没有做足够的工作,试试Collapse Hierarchy(折叠继承体系)来消除继承

对几乎没用的组件,应该以Inline Class(内联化类)来对付它们


Speculative Generality(夸夸其谈未来性)

因企图以各种各样的钩子和特殊情况来处理一些非必要的事情

场景与处理

如果某个抽象类其实没有太大作用,请运用Collapse Hierarchy(折叠继承体系)

不必要的委托可运用Inline Class(内联化类)除掉

如果函数的某些参数未被勇士,可对他实施Remove Parameter

如果函数名称带有多余的抽象意味,应该对它实施Rename Method,让它现实一点

e.g.:如果函数或类的唯一用户是测试用例,除非是帮助测试用例检测正当功能,否则就得连同其测试用例一并删除


Temporary Field(令人迷惑的暂时字段)

场景与处理

如果一个对象其内某个实例变量仅为某种特定情况而设

使用Extract Class为这个变量造一个家,然后把所有和这个变量相关的代码都放在这个新家。也许你还可以使用Introduce Null Object在“变量不合法”的情况下创建一个Null对象,从而避免写出条件式代码

如果类中有一个复杂的算法,需要好几个变量,这些变量只在使用该算法的时候有效

可以利用Extract Class把这些变量和其相关函数提炼到一个独立类中。提炼后的新对象将是一个函数对象


Message Chains(过度耦合的消息链)

消息链:用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象

场景与处理

一长串getThis()或一长串临时变量,代码将与朝着过长的导航结构紧密耦合

应该使用Hide Delegate(隐藏“委托关系”),可以在消息链的不同位置进行。理论上可以重构消息链上的任何一个对象,但这样做往往把一系列对象(intermediate object)都变成Middle M安。

先观察消息链最终得到的对象是用来干什么的,看看能否以Extract Method把使用该对象的代码提炼到独立函数中,再运用Move Method把这个函数推入消息链。如果这个链上的某个对象有多个客户打算航行此航线的剩余部分,就加入一个函数来做这件事情。


Middle Man(中间人)

对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随着委托。

但可能过度运用委托

场景与处理

某个类接口有一半的函数都委托给其他类,这就是过度运用。

应该使用Remove Middle Man(移除中间人),直接和真正负责的对象打交道。

如果这样“不干实事”的函数只有少数几个

可以运用Inline Method(内联函数)把它们放进调用端。

如果这些Middle Man还有其他行为

可以运用Replace Delegation with Inheritance(以继承取代委托)把它变成实则对象的子类,这样既可以扩展原对象的行为,又不必负担那么多的委托动作


Inappropriate Intimacy(狎昵关系)

场景与处理

两个类过于亲密,花费太多时间去探究彼此的private成分,这样过分狎昵的类必须拆散

可以采用Move Method和Move Field帮它们划清界线,从而减少狎昵行为。

可以用Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)将其中一个类对另一个斩断依赖

或者运用Extract Class把两者共同点提炼到一个安全地点,让它们使用这个新类

也可以尝试使用Hide Delegate让另一个类来传递依赖

继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望

如果能让子类单独存在,请使用Replace Inheritance with Delegation(以委托取代继承)让它离开继承体系


Alternative Classes with Different Interfaces(异曲同工的类)

场景与处理

如果两个函数做同一件事情,却有着不同的签名

运用Rename Method根据它们的用途重新命名。然后反复运用Move Method将某些行为移入类,直到两者协议一致为止。

如果必须重复而冗余的移入代码才能完成这些

可以运用Extract Superclass(提炼超类)


Incomplete Library Class(不完美的类库)

有时类库构造的不够好,但我们无法修改其中的类使它完成我们希望的工作。

场景与处理

如果你只想修改类库的一两个函数,可以运用Introduce Foreign Method(引入外加函数)

如果想要添加一大堆额外行为,就得运用Introduce Local Extension(引入本地扩展)即子类化或包装


Data Class(纯稚的数据类)

所有Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。

场景与处理

如果存在public字段,立刻运用Encapsulate Field(封装字段)把它们封装起来

如果这些类内含容器类字段,检查它们是不是得到了恰当的封装,没有则运用Encapsulate Collection(封装集合)把它们封装起来

对于那些不应该被其他类修改的字段

运用Remove Setting Method(移除设值函数),然后找出这些取值/设值函数被其他类运用的地点,尝试以Move Method把那些调用行为搬移到Data Class来,如果无法搬移整个函数就运用Extract Method产生一个可被搬移的函数,不久之后你就可以运用Hide Method把这些取值/设置函数隐藏起来了。


Refused Bequest(被拒绝的遗赠)

场景与处理

如果子类不想或不需要继承,按传统说法,这意味着继承体系设计错误

需要为这个子类新建一个兄弟类,再运用Push Down Method(函数下移)和Push Down Field(字段下移)把所有用不到的函数下移推给那个兄弟,这样一来超类就只持有所有子类共享的东西

如果子类复用了超类的行为(实现),却又不愿意支持超类的接口

即使不愿意继承接口,也不要胡乱修改继承体系,应该运用Replace Inheritance with Delegation(以委托取代继承)来达到目的


Comments(过多的注释)

如果看到一段代码有着长长的注释,需要观察是否是因为这段代码很糟糕

当感觉需要撰写注释时,请先尝试重构,试着让所有注释都变的多余

场景与处理

如果需要注释来解释一块代码做了什么,试试Extract Method

如果函数已经提炼出来,但还是需要注释来解释其行为,试试Rename Method

如果需要注释说明某些系统的需求规格,试试Introduce Assertion(引入断言)

运用注释的场景

如果你不知道该做什么,这才是注释的良好运用时机

除了用来记述将来的打算外,注释还可以用来标记并无十足把握的区域

也可以在注释里写下自己“为什么做某某事”。

相关文章

网友评论

    本文标题:重构-坏的味道

    本文链接:https://www.haomeiwen.com/subject/bznzwctx.html