【笔记】《重构:改善既有代码的设计》第3章-代码的坏味道

第3章 代码的坏味道

3.1 Duplicated Code(重复代码)

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

最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。 Extract Method

另一种常见情况就是“两个互为兄弟的子类内含相同表达式”。 Extract Method,然后Pull Up Method将它推入超类内。如果代码只是相似,并非完全相同,那么就得运用Extract Method将相似部分和差异部分割开,构成单独一个函数。然后你可能发型可以运用Form Template Method获得一个Template Method设计模式。如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并使用Substitute Algorithm将其他函数的算法替换掉。

如果两个毫不相关的类出现Duplicated Code,你应该对其中一个使用Extract Class。将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数可能属于第三个类,而另两个类应该引用这第三个类。你必须决定这个函数放在哪儿最合适,并确保它安置后就不会再在其他任何地方出现。

3.2 Long Method(过长函数)

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

很久以前程序员就已经认识到:程序越长越难理解。早期的编程语言中,子程序调用需要额外开销,这使得人们不乐意使用小函数。现代OO语言几乎已经完全免除了进程内的函数调用开销。

最终的效果就是:你应该更积极地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就要把需要说明的东西写进一个独立函数中,并以其用途(而非实现方法)命名。

百分之九十九的场合里,要把函数变小,只需使用Extract Method。找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。

如果函数内有大量参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用Extract Method,最终就会把许多参数和临时变量当做参数,传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常使用Replace Temp with Query来消除这些临时元素。Introduce Parameter Object和Preserve Whole Object则可以将过长的参数列变得简洁一些。

如果你这么做了,仍然还有太多临时变量和参数,那就应该使用我们的杀手锏:Replace Method with Method Object.

如何确定该提炼那一段代码呢?一个很好的技巧:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离

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

3.3 Large Class(过大的类)

如果想利用单个类做太多事情,其内往往就会出现太多实例变量。

你可以运用Extract Class将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。 如果这个组件适合作为一个子类,你会发现Extract Subclass往往比较简单。

有时候类并非在所有时刻都使用所有实例变量。果真如此,你或许可以多次使用Extract Class或Extract Subclass。

先确定客户端如何使用它们,然后运用Extract Interface为每一种使用方式提炼出一个接口。

如果你的Large Class是个GUI类 Duplicate Observed Data

3.4 Long Parameter List(过长参数列)

有了对象,你就不必把函数需要的所有东西都以参数传递给它了,只需传给它足够的、让函数能从中获得自己需要的东西就行了。 面向对象程序中的函数,其参数列通常比在传统程序中短得多。

如果向已有对象发出一条请求就可以取代一个参数,那么你应该激活重构手法Replace Parameter with Method。 你还可以运用Preserve Whole Object将来自同一对象的一对数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object为它们制造出一个“参数对象”。

这里有一个重要的例外:有时候你明显不希望造成“被调用对象”与“较大对象”间的某种依赖关系。

3.5 Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。 为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class将它们提炼到另一个类中。

3.6 Shotgun Surgery(霰弹式修改)

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

这种情况下你应该使用Move Method和Move Field把所有需要修改的代码放进同一个类。如果眼下没有合适的类可以安置这些代码,就创造一个。通常可以运用Inline Class把一系列相关行为放进同一个类。这可能造成少量Divergent Change,但你可以轻松处理它。

Divergent Change是指“一个类受多种变化的影响”,Shotgun Surgery则是指“一种变化引发多个类相应修改”。这个两种情况下你都会希望整理代码,使“外界变化”与“需要修改的类”趋于一一对应。

3.7 Feature Envy(依恋情节)

函数对某个类的兴趣高过对自己所处类的兴趣。这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。疗法显而易见:把这个函数移至另一个地点。你应该使用Move Method把它移到它该去的地方。有时函数中只有一部分受这种依恋之苦,这时候你应该使用Extract Method把这部分提炼到独立函数中,再使用Move Method带它去它的梦中家园。

当然,并非所有情况都这么简单。一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。如果先以Extract Method将这个函数分解为数个较小函数并分别置放于不同地点,上述步骤也就比较容易完成了。

有几个复杂精巧的模式破坏了这个规则。说起这个话题,Strategy和Visitor立刻跳入我的脑海。Self Delegation也在此列。使用这些模式是为了对抗坏味道Divergent Change。最根本的原则是:总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。Strategy和Visitor使你得以轻松修改函数行为,因为它们将少量需被覆写的行为隔离开来——当然也付出了“多一层间接性”的代价。

3.8 Data Clumps(数据泥团)

这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。首先请找出这些数据以字段形式出现的地方,运用Extract Class将它们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用Introduce Parameter Object或Preserve Whole Object为它减肥。

3.9 Primitive Obsession (基本类型偏执)

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

对象技术的新手通常不愿意在小任务上运用小对象。 你可以运用Replace Data with Object将原本单独存在的数据值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。(笔记注释:翻译的人怕不是对炙手可热这个词有什么误解。顺便多吐槽一点——你敢不敢全文都用英文原文表示算了……)如果想要替换的数据值是类型码,而它并不影响行为,则可以运用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。

3.10 Switch Statements(switch惊悚现身)

面向对象程序的一个最明显特征就是:少用switch(或case)语句。从本质上说,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。

3.11 Parallel Inheritance Hierarchies(平行继承体系)

Parallel Inheritance Hierarchies其实是Shotgun Surgery的特殊情况。在这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。

消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。如果再接再厉运用Move Method 和 Move Field,就可以将引用端的继承体系消弭于无形。

3.12 Lazy Class(冗赘类)

你所创建的每一个类,都得有人去理解它、维护它,这些工作都是要花钱的。如果一个类的所得不值其身价,它就应该消失。 如果某些子类没有做足够的工作,试试Collapse Hierarchy。对于几乎没用的组件,你应该以Inline Class对付它们。

3.13 Speculative Generality(夸夸其谈未来性)

如果你的某个抽象类其实没有太大作用,请运用Collapse Hierarchy。不必要的委托可运用Inline Class除掉。如果函数的某些参数未被用上,可对它实施Remove Parameter。如果函数名称带有多余的抽象意味,应该对它实施Rename Method,让它现实一些。

如果函数或类的唯一用户是测试用例,这就飘出了坏味道Speculative Generality。如果你发现这样的函数或类,请把它们连同其测试用例一并删掉。但如果它们的用途是帮助测试用例检测正当功能,当然必须刀下留人。

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

有时你会看到这样的对象:其内某个实例变量仅为某种特定情况而设。

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

如果类中有一个复杂算法,需要好几个变量,往往就可能导致坏味道Temporary Field的出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中。但是这些字段只在使用该算法时才有效,其他情况下只会让人迷惑。这时候你可以利用Extract Class把这些变量和其相关函数提炼到一个独立类中。提炼后的新对象将是一个函数对象。

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

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串getThis()或一长串临时变量。

这时候你应该使用Hide Delegate。你可以在消息链不同位置进行这种重构方法。理论上可以重构消息链上任何一个对象,但这么做往往会把一系列对象(intermediate object)都变成Middle Man。通常更好的选择是:先观察消息链最终得到的对象是用来干嘛的,看看能否以Extract Method把使用该对象的代码提炼到一个独立函数中,再运用Move Method 把这个函数推入消息链。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个函数来做这件事。

有些人把任何函数链都视为坏东西,我们不这样想。

3.16 Middle Man(中间人)

封装往往伴随委托。

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

3.17 Inappropriate Intimacy(狎昵关系)

两个类过于亲密,花费太多时间探究彼此的private成分。

你可以采用Move Method和Move Field帮它们划清界限,从而减少狎昵行径。你也可以看看是否可以运用Change Bidirectional Association to Unidirectional 让其中一个类对另一个斩断情丝。如果两个类实在是情投意合,可以运用Extract Class把两者的共同点提炼到一个安全地点,让它们坦荡地使用这个新类。或者也可以尝试运用Hide Delegate让另一个类为它们传递相思情。

继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望。如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation 让它离开继承体系。

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

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

3.19 Incomplete Library Class(不完美的库类)

复用常被视为对象的终极目的。不过我们认为,复用的意义经常被高估——大多数对象只要够用就好。

如果你只想修改库类的一两个函数,可以运用Introduce Foreign Method;如果想要添加一大堆的额外行为,就得运用Introduce Local Extension。

3.20 Data Class(幼稚的数据类)

所谓的Data Class是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐地操控着。这些类早期可能拥有public 字段,果真如此你应该在别人注意到它们之前,立刻运用Encapsulate Field将它们封装起来。如果这些类内含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用Encapsulate Collection把它们封装起来。对于那些不该被其他类修改的字段,请运用Removing Setting Method。

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

3.21 Refused Bequest(被拒绝的遗赠)

子类应该继承超类的函数和数据。但如果它们不想继承或不需要继承,又该怎么办呢?

按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,再运用Push Down Method 和 Push Down Field把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。你常常会听到这样的建议:所有的超类都应该是抽象(abstract)的。

我们不建议你每次都这么做。 如果Refused Bequest引起困惑和问题,请遵循传统忠告。但不必认为你每次都必须这么做。十有八九这种坏味道很淡,不值得理睬。

如果子类复用了超类的行为(实现),但却不愿意支持超类的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承超类的实现,这一点我们不介意;但如果拒绝继承超类的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承体系,应该运用Replace Inheritance with Delegation来达到目的。

3.22 Comments(过多的注释)

你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。

Comments可以带我们找到本章先前提到的各种坏味道。找到坏味道后,我们首先应该以各种重构方法把坏味道去除。完成之后我们常常发现:注释已经变得多余了,因为代码已经清楚说明了一切。

如果你需要注释来解释一块代码做了什么,试试Extract Method;如果函数已经提炼出来,但还是需要注释来解释其行为,试试Rename Method;如果你需要注释说明某些系统的需求规格,试试Introduce Assertion。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值