第3章 代码的坏味道

参考资料

概念

知道"如何"不代表知道"何时",决定何时重构及何时停止和知道重构机制如何运转一样重要!

Kent Beck提出了用味道来形容重构的时机。

  • 作者并不试图制定一个何时必须重构的精确衡量标准。从经验来看,没有任何量度规矩比得上见识广博者的直觉。
  • 对于开发者,必须培养自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少行代码才算太长。

坏味道条款

重复代码

  • 同一个类的两个函数含有相同的表达式 —— Extract Method

  • 两个互为兄弟的子类内含相同表达式

    • 代码相同:两个类都使用 Extract Method ,然后使用Pull Up Method推到父类
    • 代码类似:使用 Extract Method 将相同和差异分割,然后使用Form Template Method 设计
  • 两个毫不相关的类出现Duplicated Code,需要将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。 —— Extract Method

过长的函数

拥有短函数的对象会活得比较好、比较长。解释能力、共享能力、选择能力——都是由小型函数支持的。

每当感觉需要以注释来说明点什么的时候,就把需要说明的东西写进一个独立函数中,并以其用途命名。

过大的类

一个类如果拥有太多代码,就需要将其拆分,可以先确定客户端如何使用它们,然后为每一种使用方式提炼出一个接口; 产生条件:这个类实例变量太多,必然会有Duplicated Code(重复代码) ;类内如果有太多代码,也会产生Duplicated Code,让整个类看起来混乱并最终走向死亡。

过长参数列

太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦需要更多数据,就不得不修改它,可以把函数所需要的东西通过对象传入。

  • 一旦你需要更多的数据,你就不得不去修改它。相反如果你通过传入对象,首先你的参数列表就很短,其次如果你想增加别的变量,会有可能只需要在函数中对这个参数对象多加一次请求就行了。
  • 如果向已有的对象发送一条请求可以取代一个参数,那么你应该使用Replace Parameter with Method。
  • 注意是已有的参数,不是不存在的参数。这个需要理解一下,已有的参数就是函数宿主类中的某一个对象字段,也可能是函数本身存在另一个对象参数,让这个对象来替换它。
  • 如果某些数据缺乏合理的对象归属。可以使用Introduce Parameter Object来为它们制造一个“参数对象”。

发散式变化

一旦需要修改,只需要跳到系统的某一点,只在该处做修改。这是必须要做到的抽象,若因为修改一处代码同时牵连要修改多个函数,就会带来很严重的坏味道。

如果某一个类经常因为不同的原因在不同的方向上发生变化,就会出现Divergent Change,如增加一个功能需要修改多处,这时应该把针对某一外界变化的所有相应修改都放在一个类中;时刻要记住这么一句话:针对某一外界变化的所有相应修改,都应该产生在单一类中,而这个新类中的所有内容都应该反应此变化。

散弹式修改

散弹式修改类似于发散式变化,如果遇到某种变化,都必须在许多不同的类内做出许多小修改,那么面临的坏味道就是散弹式修改。

面对这样的问题,一个常用的策略就是使用与内联(inline)相关的重构—如Inline Class—把一系列相关行为放入同一个类。

依恋情结

函数对某个类的兴趣高过对自己所处类的兴趣。有时会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。这种情况可以将这个函数跟这些数据待在一起,使用Move Method把它移过去。如果函数只是一部分受影响,那可以先用Extract Method把这一部分提炼到独立的函数中,再使用Move Method移过去。

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

数据泥团

往往在开发中,很多地方会看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。

  • 对于这些数据应该有属于它们自己的对象。可以采用Extract Class将它们提炼到一个独立对象中,然后将注意力转移到函数签名上,运用引入参数对象保持对象完整为它瘦身。

  • 有用的类被创建出来,大量的重复被消除,后续开发得以加速,原来的数据泥团就会在其范围能发挥更大的价值。

基本类型偏执

大多数编程环境都有两种数据:结构类型允许你将数据组织成有意义的形式;基本类型则是构成结构类型的积木块。

  • 结构总是会带来一定的额外开销,它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦。
  • 将原本单独存在的数据值替换成对象,编写小对象来替换基本类型。

switch惊悚现身

任何switch语句都应该用**以多态取代条件表达式)**消除掉。甚至所有条件逻辑都应该用多态取代。

  • 我们关注的是重复的switch:在不同的地方反复使用同样的switch逻辑。

  • 重复的switch的问题在于:每当想增加一个选择分支时,必须找到所有的switch,并逐一更新。

平行继承体系

Parallel Inheritance Hierarchies其实是Shotgun Surgery的特殊情况。意思就是当你为某一个类增加子类的同时你必须为别的类同时增加子类

  • 某个类的继承体系前缀和另外一个继承体系前者完全相同,你便闻到了这股坏味道。

  • 解决这个办法的一般策略就是让一个继承体系的实例去引用另外一个继承体系的实例。然后不断运用Move Method和Move Field到被引用端,你就可以将引用端的继承体系完全打破,做到被引用端单一的继承体系。

冗赘类

虽然面向对象世界带给我们对象的魅力,但并不是类越多就越好。虽然加入间接层可以带来各种方便,但所有的类都是需要人能够去理解和维护的。

  • 对于那些实际作用不大的类,或者因为某些重构原因让它变得没有价值的时候,或开发者事前规划了某些类来应对变化,但实际上并没有发生这些变化。
  • 不论上述哪一种原因,就让这个类消失好了,这样减少你的工作量的同时也在减少别人的工作量,因为很有可能将来维护代码的人还是你自己。如果子类没有做足够的工作,可以运用Collapse Hierarchy来打破继承体系,对于几乎没有用的组件,你可以运用Inline Class来对付它们。

夸夸奇谈通用性

存在一些方法或参数是用于在未来某一天会实现的,但暂时还未实现就写了进去,这会加深对系统的理解和维护。应该搬移掉。

  • 如果抽象类没有太大作用,运用折叠继承体系。不必须要的委托可运用内联函数内联类除掉。

  • 如果函数的某些参数未被用上,可以用改变函数声明去掉这些参数。如果有并非真正需要、只是为不知远在何处的将来而塞进去的参数,应该用改变函数声明去掉。

令人迷惑的暂时字段

有些类:其内部某个字段仅为某种特定情况而设,违背了通常认为对象在所有时候都需要它的所有字段的思维。

使用提炼类搬移函数把所有和这些字段相关的代码都放到单独的类中统一管理。

过度耦合的消息链

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

  • 容易造成的问题很明显,客户端代码将与查找过程中的导航结构紧密耦合。如果对象间的关系发生变化,客户端就必须做出修改。

  • 使用隐藏委托关系。先观察消息链最终得到的对象是用来干什么的,看能否提炼函数把使用该对象的代码提炼到一个独立的函数中,在运用搬移函数把这个函数推入消息链。

中间人

对象的基本特征之一就是封装--对外部世界隐藏其内部细节。封装往往伴随着委托。而过度运用委托,如某个类的接口有一半的函数都委托给其他类。

使用移除中间人,直接和真正负责的对象打交道。如果这些函数比较少,可以运用内联函数把它们放进调用端。

狎昵关系

两个类关系太过紧密,一个类过于关注另外一个类的成员。

  • 如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些公用的数据放在一个管理良好的地方;或者用隐藏委托关系,把另一个模块变成两者的中介。

异曲同工的类

不同的类或函数,做着相同的事情。

不完美的类库

类库设计不可能完全,设计者无法预测所有情况。

  • 修改类库的一两个函数 - 引入外部函数(Introduce Foreign Method);添加一大堆额外行为 - 添加本地扩展(Introduce Local Extension)

纯稚的数据类

数据类不应该把全部字段单纯的通过getter/setter暴露出来,而应该暴露抽象接口,封装内部结构。

被拒绝的遗赠

子类应该继承超类的函数和数据。但如果继承的函数和数据有子类不需要的,意味着继承体系设计错误。

  • 如果子类不想继承父类的接口,只是利用了父类的一些行为,考虑用委托替换继承。

过多的注释

注释常常被用于错误的使用,包括因为代码设计的很糟糕,才备注了常常的注释。

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

  • 只有在不知道该做什么,才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记并无十足把握的区域。可以记述下自己"为什么这么做"。帮助将来的修改者。