重构-坏代码的味道

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构-坏代码的味道相关的知识,希望对你有一定的参考价值。

代码的坏味道

何时必须重构?没有任何标准能比得上一个见识广博者的直觉。而某些迹象,则会指出“这里有可以用重构解决的问题”,一共22条坏代码味道。

Duplicated Code(重复代码)

如果你在一个以上的地点看到相同的程序结构,那么可以肯定,将它们合而为一,程序会变得更好。

最单纯的重复代码就是,同一个类的两个函数含有相同的表达式。这时需要采用Extract Method(提取方法)提取重复代码。

另一个常见的情况是,同个父类的两个子类内含有相同表达式。这是需要使用Extract Method(提取方法),然后使用Pull Up Method(方法上移),推入父类。

如果代码只是类似,并非完全形同,那么就得运用Extract Method(提取方法) 将相似部分和差异部分分割开,单独构成一个函数。

然后可以运用Form Template Method(表单模板方法)获得一个模板方法设计模式。

如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并用Substitute Algorithm(替换算法)将其他函数替换掉

如果两个毫不相关的类出现Duplicated Code(重复代码),应该考虑对其中一个使用Extract Class(提炼类型),将复制代码提炼到一个独立类中,然后在另一个类使用这个新类。

但是重复代码所在的函数可能的确属于某个类,另一个类只能调用它。你必须决定这个函数放在那里最合适。

Long Method(过长函数)

程序越长越难理解。小函数容易理解的真正关键在于一个好名字,读者可以通过名字了解函数的作用。

最终效果是:应该更积极地分解函数。每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写在一个独立函数中,并以其用途命名。

大部分场合中,要把函数变小,只需要使用Extract Method(提炼函数)。找到函数中适合集中在一起的不分,把它们提炼出来形成一个新函数。

如果函数内有大量的参数和临时变量,他们会对你的函数提炼形成障碍。如果使用提取方法,会把变量当做参数传递。

此时,可以运用Replace Temp with Query(以查询取代临时变量)来消除这些临时元素。

Introduce Paramter Object(引入参数对象)和Preserve Whole Object(保持对象完整)可以将过长的参数列变得更简洁一些。

如果上诉方法无效,就应该使用 Replace Method with Method Object(以函数对象代替函数)

如何确定该提炼那一段代码?

一个很好的技巧就是寻找注释。通常能指出代码用途和实现手法之间的语义距离。

如果代码前方有一行注释,就是提醒你:可以将这段代码替换成一个函数,并且可以在注释的基础上给函数命名。

就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。

条件表达式和循环常常也是提炼的信号,可以使用Decompose Conditional(分解条件表达式)处理条件表达式。可以将循环和其内的代码提炼到一个独立函数中。

Large Class(过大的类)

如果利用单个类做太多事情,其内往往就会出现太多实例变量。Duplicated Code(重复代码)也就接踵而至。

可以运用Extract Class(提炼类型)将几个变量一起提炼到新类内。提炼时应选择类内彼此相关的变量。

通常如果类内的数个变量有着相同的前缀或字尾,就有机会把它们提炼到某个组件内。

如果这个组件适合作为一个子类,Extract Subclass(提炼子类)会比较简单。

有时候并非所有时刻都使用所有实例变量。那么可以多次使用Extract Class(提取类)或Extract Subclass(提取子类)。

Long Parameter List(过长参数列)

不必把函数需要的所有东西以参数传递,只需要给足够的,让函数能从中获取自己需要的东西就可以了。

如果向已有的对象发出一条请求就可以取代一个参数,那么应该使用Replace Parameter with Method(以函数代替参数)。

还可以运用Preserve Whole Object(保持对象完整)将来自同一个对象的一堆数据收集起来。

如果某些数据缺乏合理的对象归属,可以使用Interduce Parameter Object(引入参数对象)为它们制造一个参数对象。

如果你不希望造成“被调用对象”与“较大对象”间的某种依赖关系。使用参数也合情合理。

Divergent Change(发散式变化)

一旦需要修改,我们希望能够跳到系统的某一点,只在该出做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻中的一种了。

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

同一个类中,如果新加一个数据库,需要修改三个函数。新家一个工具,修改四个函数。那么将这个对象分成两个会更好。每个对象可以只因一种变化而需要修改。

针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内所有内容都应该此变化。应该找出所有变化,然后使用Extract Class(提取类)

Shotgun Surgery(霰弹式修改)

如果遇到某种变化,你都必须在许多不同的类内做出许多小修改,那么你面临的坏味道就是Shotgun Surgery。

如果需要修改的代码散布四处,你不但很难找到他们,也很容易忘记某个重要的修改。

这种情况下应该使用Move Method(搬移函数)和Move Field(搬移字段)把所有需要修改的代码放进同一个类。如果没有就创建一个。

Divergent Change和Shotgun Surgery区别

Divergent Change是指“一个类受多种变化的影响”,Shotgun Surgery是指“一种变化引发多个类相应修改”。

这两种情况你都会希望整理代码,使“外界变化”与“需要修改的类”一一对应。

Feature Envy(依恋情结)

函数对某个类的兴趣高过对自己所处类的兴趣。使用Move Method(搬移函数)把它移到该去的地方。称为依恋情结。

函数中只有一部分受依恋情结之苦,这时候应该使用Extract Method(提取方法)把这一部分提炼到独立函数中,在使用Move Method(搬移函数)

如果一个函数用到几个类的功能。原则是:判断哪个类拥有最多被此函数使用的数据,然后把这个函数和那些数据摆在一起。

策略模式和访问者模式,就是为了对抗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 Subclasses(以子类取代类型码)或 Replace Type Code with State/Strategy(以状态模式/策略模式取代类型码)加以处理。

如果你有一组总是被放在一起的字段,可运用Extract Class(提炼类)。如果你在参数列中看到基本型数据,可运用Introduce Parameter Object(引入参数对象)。

如果你发现自己正从数组中挑选数据,可运用Replace Array with Object(以对象取代数组)

Switch Statements(switch惊悚现身)

少用switch语句,问题在于重复。经常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case,就必须找到所有的switch语句并修改它们。

使用面向对象中的多态概念可解决此方法。大多数时候,看到switch语句,就应该考虑以多态来替换它。

switch语句常常根据类型码进行选择。所以应该使用Extract Method(提炼函数)将switch语句提炼到一个独立函数中,再以Move Method(搬移函数)将它搬移到需要多态性的那个类里。

此时你必须决定是否使用Replace Type Code with Subclasses(以子类取代类型码)或Replace Type Code with State/Strategy(以状态模式/策略模式取代类型码)。

一旦完成继承结构后,就可以运用Replace  Conditional with Plomorphism(以多态取代条件表达式)。

如果只是在单一函数中使用,并且不想改动它们。可以使用Replace Parameter with Explicit Methods(以明确函数取代参数)。

如果选择条件之一是null,可以使用Introduce Null Object(引入Null对象)

Parallel Inheritance Hierarchies(平行继承体系)

平行继承体系是霰弹式修改的特殊情况。在这种情况下每当你为某个类添加一个子类,也必须为另一个类添加相应的子类。

如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是问到了这种坏味道。

消除这种重复性的策略是,让一个继承体系引用另一个继承体系。或者使用Move Method(搬移函数)和Move Field(搬移字段)。

Lazy Class(冗赘类)

如果某些子类没有做足够的工作,可以使用Collapse Hierarchy(折叠继承体系)。

对于几乎没有用的组件,可以使用Inline Class(将类内联化)

Speculative Generality(夸夸其谈未来性)

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

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

如果函数的某些参数未被使用上,可使用Remove Parameter(移除参数)

如果函数的名称带有多余的抽象以为,应该对他实施Rename Method(函数改名)

如果函数或类的唯一用户是测试用例,这就是明显的Speculative Generality。需要将它和对应的测试用例一并删掉。

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

某个变量仅为某种特定情况而设。通常认为对象在所有时候都需要它的所有变量,在变量未被使用的情况下猜测当初其设置目的,会让你发疯。

如果类中有一个复杂算法,需要好几个变量。可以使用Extract Class(提炼类)还可以使用Introduce Null Object(引入Null对象)

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

如果向一个对象请求另一个对象,这就是消息练。如果长串的消息链,意味着客户代码与查找过程中的导航结构紧密耦合。

一旦对象发生变化,客户端就不得不做出相应修改。这时应该使用Hide Delegate(隐藏委托关系)。

先观察消息链最终得到的对象是用来干什么的,看看能否Extract Method(提炼函数),再用Move Method(搬移函数)推入消息链。

Middle Man(中间人)

如果看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。这个时候应该Remove Middle Man(移除中间人)

如果这样的函数很少,可以使用Inline Method(内联函数)把他们放进调用段。如果这些中间人还有其他行为,可以使用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指拥有一些字段,以及用于访问的函数。除此之外一无长物。类似于model层。

如果这些类拥有public字段,应该运用Encapsulate Field(封装字段)

如果这些类内含有容器类的字段,应该检查是否进行了恰当的封装。如果没有就运用Encapsulate Collection(封装集合)

对于不该被其他类修改的字段,运用Remove Setting Method(移除设值函数)

然后找出这些函数被其他类调用的地点,尝试Move Method(搬移函数)把这些取值/设值隐藏起来

Refused Bequest(被拒绝的遗赠)

如果子类复用了父类的行为,却又不愿意支持父类的接口。这就意味着继承体系设计错误,需要新建一个子类,

然后使用Push Down Method(函数下移)和Push Down Field(字段下移)把所有用不到的函数下推给子类。

这样父类就只持有所有子类共享的东西。建议:所有父类都应该是抽象的

Comments(过多的注释)

如果你需要注释来解释一块代码做了什么,试试Extract Method(提炼函数)。

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

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

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

以上是关于重构-坏代码的味道的主要内容,如果未能解决你的问题,请参考以下文章

我的重构识别代码的坏味道

重构·改善既有代码的设计.02之代码的“坏味道”

重构的素养

重构——代码坏味道&重组函数.md

学习重构-代码的坏味道

重构:改善既有代码的设计读书笔记——开篇