《重构:改善既有代码的设计》(二) 代码的坏味道

 

代码的坏味道:

1、Duplicated Method(重复代码)

  (1)同一个类的两个函数含有相同的表达式  用  Extract Method(110)

  (2)两个互为兄弟的子类内含相同表达式  两个类都用 Extract Method,然后对用Pull Up Method(332)推入超类

    两个互为兄弟的子类内含类似表达式  两个类都用 Extract Method 将相似和差异部分隔开。

            运用Form Template Method(345) 获得一个Template Method设计模式。

    不同的算法作相同的事,选择较清晰的一个,Substitute Algorithm(139) 将其他替换掉

  (3)不相关的类方法重复。其中一个类使用Extract Class,重复代码提炼到一个独立类中,在另一个类中使用这个新类。

    或者这个代码本就应在在两者之一的类中,而不是新的类,需要判断。

 

2、Long Method(过长函数)

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

   程序愈长愈难理解。过短函数让代码阅读者必须经常转换上下文去看看子程序做了什么,然短函数容易理解的真正关键在于一个好名字。如果你能给函数起个好名字,读者就可以通过名字了解函数的作用,根本不必去看其中写了些什么。

  可遵循的一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非其实现手法)命名。

哪怕替换后的函数调用比函数自身还长,只要函数名称能够解释其用途,就要做——关键在函数“做什么”和“如何做“之间的语义距离。

  函数内如果有大量的参数和临时变量,会传递给新函数的形参,对你的函数提炼形成阻碍,导致可读性几乎没有任何提升 ——可以使用Replace Temp with Query(120) 来消除临时元素。Introduce Parameter Object(295) 和 Preserve Whole Object(288) 则可以将过长的参数列变的更简洁些。
 
  如果仍然有太多临时变量和参数,那就应该 Replace Method With Method Object(135) “用方法对象替换方法”。
 
  如何确定该提炼哪一段代码?
    寻找注释
    寻找条件式:Decompose Conditional (238) 分解条件  
    寻找循环:将循环和其内代码提炼到一个函数中
 

3、Large Class(过大的类)

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

  可以将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。例如depositAmount和depositCurrency可能应该隶属同一个类。

  通常如果类内的数个变量有着相同的前缀或字尾,这就意味着有机会把它们提炼到某个组件内。如果这个组件适合作为一个子类,你会发现Extract Subclass往往比较简单。

  如果类内有太多代码,最简单的解决方案是把东西消弭于类内部,比如有五个“百行函数”,它们之中很多代码都相同,那么或许你可以把它们变成五个“十行函数”和十个提炼出的“双行函数”。

  和"拥有太多实例变量"一样,一个类如果拥有太多代码,往往也适合使用 Extract Class和Extract Subclass. 这里有个技巧:先确定客户端如何使用它们 ,然后运用为每一种使用方式提炼出一个接口。这或许可以帮助你看清楚如何分解这个类。

  如果你的Large Class是一个GUI类,你可能需要把数据和行为移到一个独立的领域对象去。你可能需要两边各保留一些重复数据,并保持两边同步。Duplicate

Observed Data(189)

 

4、Long Parameter List(过长参数列)

  太长的参数列难以理解,会造成前后不一致、不易使用。而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为你很可能只需在传入对象内增加一两个字段,就能得到更多数据。

  如果向已有的对象发出一条请求就可以取代一个参数,那么你应该激活重构手法Replace Parameter with Method(292)。在这里,"已有的对象"可能是函数所属类内的一个字段,也可能是另一个参数。你还可以运用Preserve Whole Object(288)将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object(295)为它们制造出一个"参数对象"。

 

5、Divergent Change(发散式变化)

  一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改 。

  如果某个类经常因为不同的原因在不同的方向上发生变化(有需求A,改3个函数。又有了需求B,要改4个函数),发散式变化就出现了。

  针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。为此,你应该找出某特定原因而造成的所有变化,然后运用Extract Class 将它们提炼到另一个类中。

 

6、Shotgun Surgery(霰弹式修改)

  每遇到某种变化,都必须在许多不同的类内做出许多小修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易忘记某个重要的修改。

  使用Move Method(142) 和Move Field(146) 把所有需要修改的代码放进同一个类.如果眼下没有合适的类可以安置这些代码,就创造一个. 通常可以运用lnline Class(154) 把一系列相关行为放进同一个类。可能会造成少量 Divergent Change,  但你可以轻易处理它。

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

 

7、Feature Envy(依恋情结)

  有一种经典的气味是:函数对某个类的兴趣高过对自已所处类的兴趣。这种依恋之情最通常的焦点便是数据:某个函数为了计算某个值, 从另一个对象那儿调用很多取值函数 。

  办法:把这个函数移至另一个地点。使用Move Method(142) 把它移到它该去的地方。有时候函数中只有一部分依恋,使用Extract Method(110)把这一部分提炼到独立函数中,再使用Move  Method(142) 移到另一个类。

  当然,并非所情况都这么简单。 一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢? 原则: 哪个类拥有最多被此函数使用的数据 ,然后就把这个函数和那些数据摆在一起。最好先以Extract Method(110) 将这个函数分解为数个较小函数,分别置放于不同类。

  有几个复杂精巧的模式破坏了这个规则。 GoF[Gangof Four) 的 Strategy和Visitor。最根本的原则是:将总是一起变化的东西放在一块儿,数据和引用这些数据的行为总是一起变化的,但也有例外 。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。 Strategy和Visitor使你得以轻松修改函数行为 ,因为它们将少量需被覆写的行为隔离开来。当然也付出了"多一层间接性" 的代价。

 

8、Data Clumps(数据泥团)

  你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自已的对象。

  首先请找出这些数据以字段形式出现的地方 ,运用Extract Class 将它们提炼到一个独立对象中。然后将注意力转移到函数签名上 ,运用Introduce  Parameter Object(295)或Preserve Whole Object(288) 为它减肥。这么做的直接好处是可以将很多参数列缩短,简化函数调用。 不必在意Data Clumps只用上新对象的一部分字段,只要以新对象取代两个(或更多)字段 ,你就值回票价了。

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

 

9、Primitive Obseession(基本类型偏执)

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

  对象技术的新手通常不愿意在小任务上运用小对象,像是结合数值和币种的 money类、 由一个起始值和一个结束值组成的 range类、电话号码或邮政编码等等的特殊字符串。

  运用Replace Data Value with Object将原本单独存在的数据值替换为对象。

  如果想要替换的数据值是类型码,而它并不影响行为 ,运用Replace Type Code with Class(218)将它换掉。

  与类型码相关的条件表达式, 可运用Replace Type Code with Subclass(213)或Replace Type Code with State/Strategy(227) 加以处理 。

  一组总是被放在一起的字段 ,可运用Extract Class。

  在参数列中看到基本型数据,不妨试试Introduce Parameter Object(295)。

  如果正从数组中挑选数据, 可运用Replace Array with Object (186)。

  

  用对象代替基本类型,并不是代替单个的基本类型,而是几个基本类型放在一起更有意义时。比如“电话号码”由基本号码,区号,地区等一起来描述时,就组装成对象,比每次都用基本类型字段描述更好。

 

10、Switch Statements(switch惊悚现身)   并不是所有switch都不应该存在

  面向对象程序的一个最明显特征就是:少用switch语句。

  从本质上主,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的 case子句,就必须找到所有switch语句并修改它们。 面向对象中的多态概念可为此带来优雅的解决办法。

  switch语句常常根据类型码进行选择,你要的是" 与该类型码相关的函数或类",使用Extract  Method将switch语句提炼到一个独立函 数中,再以Move Method 将它搬移到需要多态性的那个类里。此时你必须决定是否使用Replace Type Code with Subclasses(223)或Replace Type Code with State/Strategy(227)。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism(255)了。

 

11、Parallel Inhertance Hierarchies(平行继承体系)

  散弹式修改(Shotgun Surgery)的特殊情况。在这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。特征:某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同。

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

 

12、Lazy Class(冗赘类)

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

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

 

13、Speculative Generality(夸夸其谈未来性)

  如果你的某个抽象类其实没有太大作用,请运用 Collapse Hierarch (折叠继承体系)。不必要的委托可运用 Inline Class (将类内联化)除掉。如果函数的某些参数未被用上,可对它实施 Remove Parameter (移除参数)。如果函数名称带有多余的抽象意味,应该对它实施Rename Method (函数改名)

         如果函数或类的唯一用户是测试用例, 把它们连同其测试用例一并删除。但如果它们的用途是帮助测试用例检测正当功能,则不能删除。

 

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

  其内某个实例变量仅为某种特定情况而设。

     使用Extract Class (提炼类),然后把所有和这些变量相关的代码都放入。还可以使用 Introduce Null Object (引入Null 对象)在变量不合法的情况下创建一个null对象,从而避免写出条件式代码。

        临时字段(Temporary Field)仅供函数内某个特定的算法时,才有效。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段,使人迷惑。

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

 

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

  用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象.....这就是消息链。

  实际代码中你看到的可能是一长串getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改。

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

 

16、Middle Man(中间人)

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

  某个类接口有一半的函数都委托给其他类,这就是过度委托。 使用 Remove Middle Man(160) (移除中间人),直接和真正负责的对象打交道。

  如果不干实事的函数只有少数几个,可以运用 Inline Method(117) (内联函数)把它们放进调用端。

  如果这些中间人还有其他行为,可以运用 Replace Delegation with Inheritance(335) (以继承取代委托)把它们变成实责对象的子类,这样你即可以扩展原对象的行为,又不必负担那么多的委托动作。

 

17、Inappropriate Intimacy(狎昵关系)

  一个类过多访问另一个类的私有成员。

  以采用Move Method (搬移函数)和 Move Field (搬移字段)帮他们划清界限。你也可以看看是否可以运用 Change Bidirectional Association to Unidirectional(200)(将双向关联改为单向关联)让其中一个类对另一个斩断情丝。如果2个类实在是情投意合,可以运用 Extract Class (提炼类)把2者共同点提炼到一个安全地点,让它们坦荡的使用这个新类。或者运用 Hide Delegate(157)(隐藏委托关系)让另一个类来为它们传递相思情。

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

 

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

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

 

19、Incomplete library class(不完美的库类)

  复用常被视为对象的终极目的。不过复用的意义常被高估:大多数对象只要够用就好。但是无可否认,许多编程技术都建立在程序库的基础上。

  库类构筑者没有未卜先知的能力,因此,库往往构造得不够好,而且往往不可能让我们修改其中的类使它完成我们希望的工作。这时有两个重构方法可采用

  (1)如果你只想修改库类的一两个函数,可以运用Introduce Foreign Method(162);

  (2)如果想要添加一大堆额外行为,就得运用Introduce Local Extension(164)。

 

20、Data Class(纯稚的数据类)

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

  这些类早期可能拥有public字段,果真如此你应该在别人注意到它们之前,立刻运用 Encapsulated Field(206) (封装字段)将它们封装起来。

  如果这些类含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用 Encapsulated Collection(208) (封装集合)把它们封装起来。

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

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

 

21、Refused Bequest(被拒绝的遗赠)

  子类继承了父类的函数和数据,但子类只从中挑选几样来玩,不需要接受父类这么多的东西。按传统做法:你需要为这个子类新建一个兄弟类,再运用Push Down Method(328)和Push Down Field(329)把所有用不到的函数和变量下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。但是作者不建议你每次都这么做,这种坏味道十之八九很淡,不值得理睬。

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

22、Comments(过多的注释)

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

 

  不是注释不好。而是人们常把它当做除臭剂来使用。常有这样的情况:一段代码有着长长的注释,这些注释之所以存在是因为代码很糟糕。

 

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

 

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

   如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自已“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。

 

 

 

 

Extract Method(110) 提取出函数

Inline Method(117) 内联函数

Replace Temp with Query(120) 用查询替代临时变量

Replace Method With Method Object(135) 用方法对象替换方法

Substitute Algorithm(139)  代理算法

Extract Class(149) 提取新的类(当类太庞大的时候)

Inner Class(154) 融入进另一个类

Hide Delegate(157) 隐藏委托关系

Remove Middle Man(160) 移除中间人

Introduce Foreign Method(162) 引入外加函数     想加入一两个函数 (和下边这个164:只有当不能访问某个类的源码,又想给它添加新的责任的时候才会用)

Introduce Local Extension(164) 引入本地扩展  想加入不止一两个函数

Replace Array with Object (186) 将数组替换为对象

Duplicate Observed Data(189) 重复数据

Change Bidirectional Association to Unidirectional(200)将双向关联改为单向关联

Replace Type Code with Subclass(213) 替换为子类

Replace Type Code with Class(218)替换为类
Replace Type Code with State/Strategy(227)  替换为状态/策略
Decompose Conditional (238) 分解条件
Replace Conditional  with Polymorphism(255) 用多态替代条件语句
 Introduce Assertion(267) 引入断言

Preserve Whole Object(288)  

Introduce Parameter Object(295)

Push Down Method(328)  将函数推到子类中

Push Down Field(329) 将变量 推到子类中

Hide Method(303) 隐藏函数

Pull Up Method(332) 将函数提到父类中

Replace Delegation with Inheritance(335)以继承取代委托

Extract Superclass(336) 提炼超类

Form Template Method(345)  形成模板函数

Extract Interface(341) 提取成接口

Replace Inheritance with Delegation(352)以委托取代继承

 

转载于:https://www.cnblogs.com/maxiaodoubao/p/4629816.html

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值