重构系列之重构的标志:《重构》代码的坏味道

来源:互联网 发布:苏联解体对中国知乎 编辑:程序博客网 时间:2024/04/30 05:09

重复代码

1.
表现:同一个类的两个函数含有相同的表达式。
方案:提炼函数。

2.
表现:两个互为兄弟的子类内含有相同表达式。
方案:函数上移,推入超类。

3.
表现:如果两个毫不相关的类出现重复代码。
方案:对其中一个类采用提炼类的方式,将重复代码提取到一个独立类中(类似工具类),然后在另外一个类中使用这个新类。(也可能这个函数并确实属于其中的一个类,那让另外一个类引用它即可)

过长函数

表现:函数过长
方案:提炼函数(99%)、以查询取代临时变量、引入参数对象、保持对象完整、分解条件表达式等

过大的类

表现:单个类做太多事情,其内往往就会出现太多实例变量。
方案:提炼类、提炼子类、提炼接口、复制“被监视数据”

过长参数列

表现:太长的参数列难以理解,太多参数会造成前后不一致,不易使用
方案:以函数取代参数、以一个对象取代这些参数、保持对象完整

大量全局变量

全局变量是邪恶的东西

发散式变化

表现:一个类受多种变化的影响
方案:你应该找出某特定原因而造成的所有变化,然后运用提炼类将它们提炼到另一个类中

霰弹式修改

表现:一个变化引起多个类修改。如果需要修改的代码散布四处,你不但很难找到它们,并且很容易忘记某个重要的修改。
方案:采用搬移函数、搬移字段将所有需要修改的代码放进同一个类,如果没有合适的类,就新建一个。通常可以使用将类内联化的方式将一系列相关行为放进同一个类。

依恋情结

类的方法应该去该去的地方:

表现:函数对某个类的兴趣高过对自己所处的类,通常的焦点就是数据,某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。

方案:这时一个运用 Move Method (搬移函数)把它移到自己该去的地方。有时候函数中只有一部分受这种依恋之苦,这时候使用Extract Method (提炼函数)把这部分提炼到独立函数中,再使用Move Method (搬移函数)带它去它的梦中家园。

一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。

数据泥团

众多数据项待在一块:

常常可以在很多地方看到相同的3、4项数据:2个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先找出这些数据以字段形式出现的地方,运用Extract Class (提炼类)将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object (引入参数对象)或Preserve Whole Object (保持对象完整)为它减肥。这么做的直接好处是可以将很多参数列缩短,简化函数调用。不必在意数据泥团(Data Clumps)只用上新对象的一部分字段,只要以新对象取代2(或更多)个字段,就值得了。

一个好的评判方法是:删除众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。

基本类型偏执

表现:喜欢使用基本类型,而不愿运用小对象:

方案:
对象的一个极具价值的东西是:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。你可以轻松的编写出一些与语言内置(基本)类型无异的小型类。

对象技术的新手通常不愿意在小任务上运用小对象—像是结合数值和币种的money类,由一个起始值和结束值组成的range类等。你可以使用 Replace Data Value withObject (以对象取代数据值)将原本单独存在的数据值替换为对象。如果想要替换的数据值是类型码,而它并不影响行为,则可以运用 Replace Type Code with Class (以类取代类型码)将它替换掉。如果你有与类型码相关的条件表达式,可运用 Replace Type Code with Subclass (以子类取代类型码)或 Replace Type Code with State/Strategy (以状态/策略取代类型码)加以处理。

如果你有一组应该总是被放在一起的字段,可运用Extract Class (提炼类)。如果你在参数列中看到基本数据类型,不妨试试 Introduce Parameter Object (引入参数对象)。如果你发现自己正从数组中挑选数据,可运用 Replace Array with Object(以对象取代数组).

switch 惊悚现身

问题:switch语句的问题在于重复:面向对象程序的一个最明显特征就是:少用switch或(case)语句。从本质上说,switch语句的问题在于重复。你常会发现switch语句散布于不同地点。如果要为它添加一个新的case子句,就必须找到所有switch语句并修改它们。

方案:面向对象中的多态概念可为此带来优雅的解决办法。大多数时候,一看到switch语句,就应该考虑以多态来替换它。问题是多态该出现在哪?switch语句常常根据类型码进行选择,你要的是“与该类型码相关的函数或类”,所以应该使用 Extract Method (提炼函数)将switch语句提炼到一个独立函数中,再以 Move Method (搬移函数)将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclass (以子类取代类型码)或 Replace Type Code with State/Strategy (以状态/策略取代类型码)。一旦完成这样继承结构后,就可以运用Replace Conditional with Polymorphism (以多态取代条件表达式)了。如果你只是在单一函数中有些选择事例,且并不想改动它们,那么多态就不必要了。这时可运用 Replace Parameter with Explicit Methods (以明确函数取代参数)。如果你的选择条件之一是null,可以试试 Introduce Null Object (引入Null 对象)。

平衡继承体系

问题:平行继承体系其实是散弹式修改(Shotgun Surgery)的特殊情况:在这种情况下,每当你为某个类增加1个子类,必须也为另一个类相应增加1个子类。如果你发现某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同,便是闻到了这种坏味道。

方案:消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用 Move Method (搬移函数)和Move Field (搬移字段),就可以将引用端的继承体系取消。

冗赘类

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

方案:即如果某些子类没有足够的工作,试试Collapse Hierarch (折叠继承体系)。对应几乎没用的组件,应该以Inline Class (将类内联化)对付它们。

夸夸其谈未来性

问题:如果用不到的类就不要用:

方案:如果你的某个抽象类其实没有太大作用,请运用 Collapse Hierarch (折叠继承体系)。不必要的委托可运用 Inline Class (将类内联化)除掉。如果函数的某些参数未被用上,可对它实施 Remove Parameter (移除参数)。如果函数名称带有多余的抽象意味,应该对它实施Rename Method (函数改名)如果函数或类的唯一用户是测试用例,这就飘出了坏味道 夸夸其谈未来性(Speculative Generality)。 如果有这样的函数或类,请把它们连同其测试用例一并删除。但如果它们的用途是帮助测试用例检测正当功能,则不能删除。

令人迷惑的暂时字段

问题:对象的暂时性属性经常让人迷惑:有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有变量。在变量未被使用的情况下猜测当初设置目的,会让你发疯。

方案:请使用Extract Class (提炼类)给这些变量创造一个家,然后把所有和这些变量相关的代码都放进这个新家,也许你还可以使用 Introduce Null Object (引入Null 对象)在变量不合法的情况下创建一个null对象,从而避免写出条件式代码。如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道令人迷惑的临时字段(Temporary Field)出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段。但是这些字段只在使用该算法时才有效,其他情况下只会让人迷惑。这时可以利用 Extract Class (提炼类)把这些变量和其相关函数提炼到一个独立的类中。提炼后的新对象将是一个函数对象。

过度耦合的消息链

问题:A对象请求B对象,B对象请求C对象…:如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改。

方案:这时候应该使用 Hide Delegate (隐藏委托关系)。你可以在消息链的不同位置进行这种重构。理论上可以重构消息链上任何对象,但这么做往往会把一系列对象都变成Middle Man(中间人)。通常更好的选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以 Extract Method (提炼函数)把使用该对象的代码提炼到一个独立函数中,再运用 Move Method (搬移函数)把这个函数推入消息链。

中间人

问题:过度运用委托:对象的基本特征之一就是封装:对外部世界隐藏其内部细节。封装往往伴随委托。比如说你问你主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。你没必要知道这位主管到底使用传统记事簿或电子记事簿或秘书来记录自己的约会。

方案:但是人们可能过度运用委托。你也许会看到某个类有一半的函数都委托给其他类,这样就是过度运用。这时应该使用Remove Middle Man (移除中间人),直接和真正负责的对象打交道。如果不干实事的函数只有少数几个,可以运用 Inline Method (内联函数)把它们放进调用端。如果这些中间人还有其他行为,可以运用 Replace Delegation with Inheritance (以继承取代委托)把它们变成实责对象的子类,这样你即可以扩展原对象的行为,又不必负担那么多的委托动作。

狎昵关系

问题:2个类过于亲密:

方案:有时候你会看到2个类过于亲密,花费太多时间起探究彼此的private成分。你可以采用Move Method (搬移函数)和 Move Field (搬移字段)帮他们划清界限。你也可以看看是否可以运用 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)让其中一个类对另一个斩断情丝。如果2个类实在是情投意合,可以运用Extract Class (提炼类)把2者共同点提炼到一个安全地点,让它们坦荡的使用这个新类。或者可以尝试运用 Hide Delegate (隐藏委托关系)让另一个类来为它们传递相思情。继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望,如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation (以委托取代继承)让它离开继承关系。

异曲同工的类

问题:两个函数做同一件事:

方案:如果两个函数做同一件事,却有着不同的签名,请运用 Rename Method (函数改名)根据它们的用途重新命名。但这往往不够,请反复运用 Move Method (搬移函数)将某些行为移入类,直到2者的协议一致为止。如果你必须反复而赘余的移入代码才能完成这些,或许可运用Extract Superclass (提炼超类)。

不完善的库类

问题:复用常被视为对象的终极目的。不过复用的意义常被高估:大多数对象只要够用就好。但是无可否认,许多编程技术都建立在程序库的基础上。库类的构筑者没用未卜先知的能力,不能因此责怪他们。麻烦的是库往往构造的不够好,而且往往不可能让我们修改其中的类使它们完成我们希望完成的工作。这是否意味着那些经过实践检验的技术,如今都派不上用场了?

方案:幸好我们有2个专门应付这种情况的工具。如果你只想修改类库的一两个函数,可以运用Introduce Foreign Method (引入外加函数);如果想要添加一大堆额外行为,就得运用 Introduce Local Extension (引入本地扩展) 。

纯稚的数据类

问题:所谓的Data Class是指:它们拥有一些字段,以及用于访问这些字段的函数,除此之外一无长物。这样的类只是不会说话的数据容器,它们几乎一定被其他类过分细琐的操控着。

方案:这些类早期可能拥有public字段,果真如此你应该在别人注意到它们之前,立刻运用 Encapsulated Field (封装字段)将它们封装起来。如果这些类含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用 Encapsulated Collection (封装集合)把它们封装起来。对于那些不该被其他类修改的字段,请运用 Remove Setting Method (移除设置函数)。然后,找出这些取值/设值函数被其他类运用的地点。尝试以 Move Method (搬移函数)把那些调用行为搬移到Data Class来。如果无法搬移这个函数,就运用 Extract Method (提炼函数)产生一个可搬移的函数。不久之后就可以运用 Hide Method (隐藏函数)把这些取值/设值函数隐藏起来了。

被拒绝的遗赠

问题:子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该这么办呢?它们得到所有礼物,却只从中挑选几样来玩。

方案:按传统说法,这就意味着继承体系设计误。你需要为这个子类新建一个兄弟类,再次运用 push down Method (函数下移)和 push down field (字段下移)把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有超类都应该是抽象的。既然使用“传统说法”这个略带贬义的词,你就可以猜到,我们不建议你这么做,起码不建议你每次都这么做。我们继承利用继承来复用一些行为,并发现可以很好的应用于日常工作。这也是一种坏味道,我们不否认,但气味通常并不强烈。所以我们说:如果Refused Bequest引起困惑和问题,请遵循传统忠告。但不必认为你每次都得那么做。十有八九这种坏味道很淡,不值得理睬。如果子类复用了超类的行为,却有不愿支持超类的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承超类的实现, 这点我们不介意;但如果拒绝继承超类的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承体系,应用运用 Replace Inheritance with Delegation (以委托取代继承)来达到目的。

过多的注释

问题:我们之所以在这里提到注释,是因为人们常把它当做除臭剂来使用。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。注释可以带我们找到代码中的坏味道。找到坏味道后,我们首先应该以各种重构手法把坏味道去除。完成之后我们常常会发现:注释已经变得多余了,因为代码已经清楚说明了一切。

方案:如果你需要注释来解释一块代码做了说明,试试Extract Method (提炼函数);如果函数已经提炼出来,但还是需要注释来解释其行为,试试 Rename Method (函数改名);如果你需要注释说明某些系统的需求规格,试试 Introduce Assertion (引入断言)。

0 0