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

79 阅读10分钟

神秘命名

[!info] 改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜 藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码 进行精简。

重复代码

[!info] 如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们 合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复的代码时你就 必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的 副本来修改。

过长函数

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

过长参数列表

当参数列表过长时,会给代码的可读性和可维护性带来负面影响,原因如下:

  1. 可读性差:当参数列表过长时,函数或方法的定义可能会超过一行,这会使代码难以阅读和理解。

  2. 可维护性差:当参数列表过长时,任何更改函数或方法定义的人都需要仔细检查每个参数的顺序和类型。如果参数列表很长,这可能会变得非常困难。此外,如果需要添加或删除参数,这也可能会导致其他代码中的错误。

  3. 可测试性差:当参数列表过长时,测试函数或方法时需要提供所有参数,这会使测试代码变得复杂和难以阅读。

全局数据

[!info] 全局数据印证了帕拉塞尔斯的格言:良药与毒药的区别在于剂量。有少量的 全局数据或许无妨,但数量越多,处理的难度就会指数上升。即便只是少量的数 据,我们也愿意将它封装起来,这是在软件演进过程中应对变化的关键所在。

可变数据

[!info] 对数据的修改经常导致出乎意料的结果和难以发现的bug。我在一处更新数 据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了 ——如果故障只在很罕见的情况下发生,要找出故障原因就会更加困难

发散式变化

[!info] 发散式变化(Divergent Change) 我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。一旦需要 修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这一 点,你就嗅出两种紧密相关的刺鼻味道中的一种了。

霰弹式修改

[!info] 霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,你都 必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。如 果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修 改。

依恋情结

[!info] 所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。

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

数据泥团

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

基本类型偏执

程序员常常不愿创建对问题域有用的基本类型,如钱、坐标、范围等。这导致了一些不良的编程实践,如把钱当作普通数字计算、无视单位计算物理量以及过多使用类字符串类型变量。一个体面的类型,至少能包含一致的显示逻辑,在用户界面上需要显示时可以使用。

重复的switch

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

循环语句

[!info] 如今,函数作为一等公民已经得到了广泛的支持,因此我们可以使用以管道取代循环(231)来让这些老古董退休。我们发现,管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。

冗赘的元素

冗赘的元素会使代码变得冗长、混乱,降低代码的可读性,增加代码的维护难度。

夸夸其谈通用性

[!info] 当有人说“噢,我想我 们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些 非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维 护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。用不上 的装置只会挡你的路,所以,把它搬开吧。

临时字段

[!info] 有时你会看到这样的类:其内部某个字段仅为某种特定情况而设。这样的代 码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段 未被使用的情况下猜测当初设置它的目的,会让你发疯。

过长的消息链

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

中间人

[!info] 对象的基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴 随着委托。 但是人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委 托给其他类,这样就是过度运用。

内幕交易

[!info] 软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据, 因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们 必须尽量减少这种情况,并把这种交换都放到明面上来。

过大的类

[!info] 如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重 复代码也就接踵而至了。

异曲同工的类(不同接口实现的类:Alternative Classes with Different Interfaces)

使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个 类。但只有当两个类的接口一致时,才能做这种替换。

纯数据类

[!info] 所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的 函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一 定被其他类过分细琐地操控着。 纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据 的行为从客户端搬移到纯数据类里来,就能使情况大为改观。

被拒绝的遗赠

[!info] 如果子类复用了超类的行为(实现),却又不愿意支持超类的接口,“被拒 绝的遗赠”的坏味道就会变得很浓烈。

注释

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

坏味道与重构手法速查表

坏味道(英文)坏味道(中文)常用重构
Alternative Classes with Different Interfaces异曲同工的类改变函数声明, 搬移函数, 提炼超类
Comments注释提炼函数, 改变函数声明, 引入断言
Data Class纯数据类封装记录, 移除设值函数, 搬移函数, 提炼函数, 拆分阶段
Data Clumps数据泥团提炼类, 引入参数对象, 保持对象完整
Divergent Change发散式变化拆分阶段, 搬移函数, 提炼函数, 提炼类
Duplicated Code重复代码提炼函数, 移动语句, 函数上移
Feature Envy依恋情结搬移函数, 提炼函数
Global Data全局数据封装变量
Insider Trading内幕交易搬移函数, 搬移字段, 隐藏委托关系, 以委托取代子类, 以委托取代超类
Large Class过大的类提炼类, 提炼超类, 以子类取代类型码
Lazy Element冗赘的元素内联函数, 内联类, 折叠继承体系
Long Function过长函数提炼函数, 以查询取代临时变量, 引入参数对象, 保持对象完整, 以命令取代函数, 分解条件表达式, 以多态取代条件表达式, 拆分循环
Long Parameter List过长参数列以查询取代参数, 保持对象完整, 引入参数对象, 移除标记参数, 函数组合成类
Loops循环语句以管道取代循环
Message Chains过长的消息链隐藏委托关系, 提炼函数, 搬移函数
Middle Man中间人移除中间人, 内联函数, 以委托取代超类, 以委托取代子类
Mutable Data可变封装变量, 拆分变量, 移动语句, 提炼函数, 将查询函数和修改函数分离, 移除设值函数, 以查询取代派生变量, 函数组合成类, 函数组合成变换, 将引用对象改为值对象
Mysterious Name神秘命名改变函数声明, 变量改名, 字段改名
Primitive Obsession基本偏执以对象取代基本类型, 以子类取代类型码, 以多态取代条件表达式, 提炼类, 引入参数对象
Refused Bequest被拒绝的遗赠函数下移, 字段下移, 以委托取代子类, 以委托取代超类
Repeated Switches重复的switch以多态取代条件表达式
Shotgun Surgery霰弹改搬移函数, 搬移字段, 函数组合成类, 函数组合成变换, 拆分阶段, 内联函数, 内联类
Speculative Generality夸夸通用性折叠继承体系, 内联函数, 内联类, 改变函数声明, 移除死代码
Temporary Field临时字段提炼类, 搬移函数, 引入特例