小土刀

【重构 改善既有代码的设计】读书笔记

重构:在代码写好之后改进它的设计。


尽管关注对象是代码,但重构对于系统设计也有巨大影响。资深设计师和架构师也很有必要了解重构原理,并在自己的项目中运用重构技术。最好是由老资格、经验丰富的开发人员来引入重构技术,因为这样的人最能够透彻理解重构背后的原理,并根据情况加以调整,使之适用于特定工作领域。

如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力。

重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构原则

为何重构:

  • 重构改进设计
  • 重构使软件更容易理解
  • 帮助找到 bug
  • 重构提高编程速度

何时重构:

  • 三次法则:事不过三,三则重构
  • 添加功能时重构
  • 修补错误时重构
  • 复审代码时重构

重构的难题

  • 数据库
  • 修改接口:不要过早发布接口
  • 难以通过重构手法完成的设计改动

代码的坏味道

  • 重复代码
  • 过长函数
  • 过大的类
  • 过长参数列
  • 发散式变化:类经常因为不同的原因在不同的方向上发生变化
  • 霰弹式修改:每遇到某种变化,你都必须在许多不同的类内做出许多小修改
  • 依恋情结:一个类的动作过分依赖其他类
  • 数据泥团:不同地方的相同数据字段
  • 基本类型偏执
  • Switch 惊悚现身:考虑用多态代替 switch
  • 平行继承体系:为某个类增加一个子类的时候,也必须为另一个类相应增加一个子类
  • 冗赘类
  • 夸夸其谈未来性:某个抽象类其实没啥太大作用
  • 令人迷惑的暂时字段
  • 过度耦合的消息链
  • 中间人:某个类接口有一半的函数都委托给其他类
  • 狎昵关系:两个类过于亲密
  • 异曲同工的类:两个函数做同一件事,却有着不同的签名
  • 不完美的库类
  • 纯稚的数据类:单纯的数据容器
  • 被拒绝的遗赠:子类复用超类的行为,却又不愿意支持超类的接口
  • 过多的注释:当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余

构筑测试体系

  • 确保所有测试都完全自动化,让它们检查自己的测试结果
  • 一套测试就是一个强大的 bug 侦测器,能够大大缩减查找 bug 所需要的时间
  • 频繁地运行测试。每次编译请把测试也考虑进去——每天至少执行每个测试一次
  • 每当你收到 bug 报告,请先写一个单元测试来暴露 bug
  • 编写未臻完善的测试并实际运行,好过对完美测试的无尽等待
  • 考虑可能出错的边界条件,把测试火力集中在那儿
  • 当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常
  • 不要因为测试无法捕捉所有 bug 就不写测试,因为测试的确可以捕捉到大多数 bug

重新组织函数

提炼函数 Extract Method

有一段代码可以被组织在一起并独立出来 -> 将折断代码放进一个独立函数中,并让函数名称解释该函数的用途。

  • 创造一个新函数,根据这个函数的意图来对它命名(以它『做什么』来命名,而不是以它『怎样做』命名)
  • 将提炼出的代码从源函数复制到新建的目标函数中
  • 仔细检查提炼出的代码,看看是否引用了『作用域限于源函数』的变量(包括局部变量和源函数参数)
  • 检查是否有『仅用于被提炼代码段』的临时变量。如果有,在目标函数中将它们声明为临时变量
  • 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来
  • 将被提炼代码段中需要读取的局部变量,当做参数传给目标函数
  • 处理完所有局部变量之后,进行编译
  • 在源函数中,将被提炼代码段替换为对目标函数的调用(如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了)
  • 编译,测试

内联函数 Inline Method

一个函数的本体与名称同样清楚易懂 -> 在函数调用点插入函数本体,然后移除该函数

  • 检查函数,确定它不具多态性(如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数)
  • 找出这个函数的所有被调用点
  • 将这个函数的所有被调用点都替换为函数本体
  • 编译,测试
  • 删除该函数的定义

内联临时变量 Inline Temp

你有一个临时变量,只被一个简单表示式赋值一次,而它妨碍了其他重构手法 -> 将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

  • 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
  • 如果这个临时变量并未被声明为 final,那么就将它声明为 final,然后编译(这可以检查该临时变量是否真的只被赋值一次)
  • 找到该临时变量的所有引用点,将它们替换为『为临时变量赋值』的表达式
  • 每次修改后,编译并测试
  • 修改完所有引用点之后,删除该临时变量的声明和赋值语句
  • 编译,测试

以查询取代临时变量 Replace Temp with Query

你的程序以一个临时变量保存某一表达式的运算结果 -> 将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。

  • 找出只被赋值一次的临时变量(如果某个临时变量被赋值超过一次,考虑使用 Split Temporary Variable 将它分割成多个变量)
  • 将该临时变量声明为 final
  • 编译(这可确保该临时变量的确只被赋值一次)
  • 将『对该临时变量赋值』的语句的等号右侧部分提炼到一个独立函数中(首先将函数声明为 private;确保提炼出来的函数无任何副作用,如果它有副作用,就对它进行 Separate Query from Modifier)
  • 编译,测试
  • 在该变量身上实施 Inline Temp

引入解释性变量 Introduce Explaining Variable

你有一个复杂的表达式 -> 将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途

  • 声明一个 final 临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它
  • 将表达式中的『运算结果』这一部分,替换为上述临时变量(如果被替换的这一部分在代码中重复出现,你可以每次一个,逐一替换)
  • 编译测试
  • 重复上述过程,处理表达式的其他部分

分解临时变量 Split Temporary Variable

你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果 -> 针对每次赋值,创造一个独立、对应的临时变量

  • 在待分解临时变量的声明及其第一次被赋值处,修改其名称(如果稍后的赋值语句是 i=i+expression 形式,就意味着这是个结果搜收集变量,就不要分解。结果收集变量的做用通常是累加、字符串接合、写入流或者向集合添加元素)
  • 将新的临时变量声明为 final
  • 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量
  • 在第二次赋值处,重新声明原先那个临时变量
  • 编译,测试
  • 逐次重复上述过程。每次都在声明出对临时变量改名,并修改下次赋值之前的引用点

移除对参数的赋值 Remove Assignments to Parameters

代码对一个参数进行赋值 -> 以一个临时变量取代该参数的位置

  • 建立一个临时变量,把待处理的参数值赋予它
  • 以『对参数的赋值』为界,将其后所有对此参数的引用点,全部替换为『对此临时变量的引用』
  • 修改赋值语句,使其改为对新建临时变量赋值
  • 编译,测试(如果代码的语义是按引用传递的,请在调用端检查调用后是否还使用了这个参数。也要检查有多少个按引用传递的参数被赋值后又被使用。请尽量只以 return 方式返回一个值。如果需要返回的值不止一个,看看可否把需返回的大堆数据变成单一对象,或干脆为每个返回值设计对应的一个独立函数)

以函数对象取代函数 Replace Method with Method Object

你有一个大型函数,其中对局部变量的使用使你无法采用 Extract Method -> 将这个函数放进一个单独对象中,如此依赖局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数

  • 建立一个新类,根据待处理函数的用途,为这个类命名
  • 在新类中建立一个 final 字段,用以保存原先大型函数所在的对象。我们将这个字段称为『源对象』。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存
  • 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数
  • 在新类中建立一个 compute() 函数
  • 将原函数的代码复制到 compute() 函数中。如果需要调用源对象的任何函数,请通过源对象字段调用
  • 编译
  • 将旧函数的函数本体替换为这样一条语句:『创建上述新类的一个新对象,然后调用其中的 compute() 函数』

替换算法 Substitute Algorithm

你想要把某个算法替换成一个更清晰的算法 -> 将函数本体替换为另一个算法

  • 准备好另一个(替换用)算法,让它通过编译
  • 针对现有测试,执行上述新算法。如果结果与原本相同,重构结束
  • 如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准(对于每个测试用例,分别以新旧两种算法执行,并观察两者结果是否相同)

在对象之间搬移特性

在对象的设计过程中,『决定把责任放在哪儿』即使不是最重要的事,也是最重要的事之一。

搬移函数 Move Method

你的程序中,有个函数与其所在类之外的另一个类进行更多交流:调用后者,或被后者调用 -> 在该函数最常引用的类中建立一个有类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除

  • 检查源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也该被搬迁(如果某个特性只被你打算搬移的那个函数用到,就应该将它一并搬迁。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬迁。有时候,搬移一组函数比逐一搬移简单些)
  • 检查源类的子类和超类,看看是否有该函数的其他声明(如果出现其他声明,你或许无法进行搬移,除非目标类也同样表现出多态性)
  • 在目标类中声明这个函数(你可以选择一个更有意义的名称)
  • 将源函数的代码复制到目标函数中。调整后者,使其能在新类中正常运行(如果源函数包含异常处理,你得判断逻辑上应该由哪个类来处理这一异常。如果应该由源类来负责,就把异常处理留在原地)
  • 编译目标类
  • 决定如何从源函数正确引用目标对象
  • 修改源函数,使之成为一个纯委托函数
  • 编译,测试
  • 决定是否删除源函数,或将它当作一个委托函数保留下来(如果你经常要在源对象中引用目标函数,那么将源函数作为委托函数保留下来会比较简单)
  • 如果要移除源函数,请将源类中对源函数的所有调用,替换为对目标函数的调用
  • 编译,测试

搬移字段 Move Field

你的程序中,某个字段被其所在类之外的另一个类更多的用到 -> 在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段

  • 如果字段的访问级是 public,使用 Encapsulate Field 将它封装起来(如果你有可能移动那些频繁访问该字段的函数,或如果有许多函数访问某个字段,先使用 Self Encapsulate Field 也许会有帮助)
  • 编译,测试
  • 在目标类中建立与源字段相同的字段,并同时建立相应的设值/取值函数
  • 编译目标类
  • 决定如何在源对象中引用目标对象(首先看是否有一个现成的字段或函数可以帮助你得到目标对象,如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在源类中新建一个字段来存放目标对象。这可能是个永久性修改,但你也可以让它是暂时的,因为后续重构可能会把这个新建字段除掉)
  • 删除源字段
  • 将所有对源字段的引用替换为对某个目标函数的调用
  • 编译,测试

提炼类 Extract Class

某个类做了应该由两个类做的事 -> 建立一个新类,将相关的字段和函数从旧类搬移到新类

  • 决定如何分解类所负的责任
  • 建立一个新类,用以表现从旧类中分离出来的责任(如果旧类剩下的责任与旧类名称不符,为旧类更名)
  • 建立『从旧类访问新类』的连接关系(有可能需要一个双向链接。但是在真正需要它之前,不要建立『从新类通往旧类』的链接)
  • 对于你想搬移的每一个字段,运用 Move Field 搬移之
  • 每次搬移后,编译、测试
  • 使用 Move Method 将必要函数搬移到新类,先搬移低层函数(也就是『被其他函数调用』多于『调用其他函数』的函数),再搬移较高层函数
  • 每次搬移之后,编译、测试
  • 检查,精简每个类的接口(如果你建立其双向链接,检查是否可以将它改为单向连接)
  • 决定是否公开新类。如果你的确需要公开它,就要决定让它成为引用对象还是不可变的值对象

这里也存在危险性。如果需要确保两个对象同时被锁定,你就面临事务问题,需要使用其他类型的共享锁。

将类内联化 Inline Class

某个类没有做太多事情 -> 将这个类的所有特性搬移到另一个类中,然后移除原类

  • 在目标类身上声明源类的 public 协议,并将其中所有函数委托至源类(如果『以一个独立接口表示源类函数』更合适的话,就应该在内联之前先使用 Extract Interface)
  • 修改所有源类引用点,改而引用目标类(将源类声明为 private,以斩断包之外的所有引用可能。同时修改源类的名称,这便可使编译器帮助你捕捉到所有对于源类的隐藏引用点)
  • 编译,测试
  • 运用 Move Method 和 Move Field,将源类的特性全部搬移到目标类
  • 为源类举行一个简单的『丧礼』

隐藏委托关系 Hide Delegate

客户通过一个委托类来调用另一个对象 -> 在服务类上建立客户所需的所有函数,用以隐藏委托关系

  • 对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数
  • 调整客户,令它只调用服务对象提供的函数(如果使用者和服务提供者不在同一个包)
  • 每次调整后,编译并测试
  • 如果将来不再有任何客户需要用到 Delegate,便课移除服务对象中的相关访问函数
  • 编译,测试

移除中间人 Remove Middle Man

某个类做了过多的简单委托动作 -> 让客户直接调用受托类

  • 建立一个函数,用以获得受托对象
  • 对于每个委托函数,在服务类中删除该函数,并让需要调用该函数的客户转为调用受托对象
  • 处理每个委托函数后,编译、测试

引入外加函数 Introduce Foreign Method

你需要为提供服务的类增加一个函数,但你无法修改这个类 -> 在客户类中建立一个函数,并以第一参数形式传入一个服务类实例

外加函数终究是权宜之计。如果有可能,你仍然应该将这些函数搬移到它们的理想家园。如果由于代码所有权的原因使你无法这么做,就把外加函数交给服务类的拥有者,请他帮你在服务类中实现这个函数。

  • 在客户类中建立一个函数,用来提供你需要的功能(这个函数不应该调用客户类的任何特性。如果它需要一个值,把该值当做参数传给它)
  • 以服务类实例作为该函数的第一个参数
  • 将该函数注释为『外加函数(foreign method),应该在服务类实现』(这么一来,如果将来有机会将外加函数搬移到服务类中时,你便可以轻松找出这些外加函数)

引入本地扩展 Introduce Local Extension

你需要为服务类提供一些额外函数,但你无法修改这个类 -> 建立一个新类,使它包含这些额外函数。让这个扩展品陈伟源类的子类或包装类

  • 建立一个扩展类,将它作为原始类的子类或包装类
  • 在扩展类中加入转型构造函数(所谓『转型构造函数』是指『接受原对象作为参数』的构造函数。如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数;如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象)
  • 在扩展类中加入新特性
  • 根据需要,将原对象替换为扩展对象
  • 将针对原始类定义的所有外加函数搬移到扩展类中

重新组织数据

自封装字段 Self Encapsulate Field

你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙 -> 为这个字段建立取值/设值函数,并且只以这些函数来访问字段

  • 为待封装字段建立取值/设值函数
  • 找出该字段的所有引用点,将它们全部改为调用取值/设值函数
  • 将该字段声明为 private
  • 复查,确保找出所有引用点
  • 编译,测试

以对象取代数据值 Replace Data Value with Object

你有一个数据项 -> 需要与其他数据和行为一起使用才有意义 -> 将数据项变成对象

  • 为待替换数值新建一个类,在其中声明一个 final 字段,其类型和源类中的待替换数值类型一样。让后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数
  • 编译
  • 将源类中的待替换数值字段的类型改为前面新建的类
  • 修改源类中该字段的取值函数,令它调用新类的取值函数
  • 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作
  • 修改源类中待替换字段的设值函数,令它为新类创建一个实例
  • 编译,测试
  • 现在,你有可能需要对新类使用 Change Value to Reference

将值对象改为引用对象 Change Value to Reference

你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象 -> 将这个值对象编程引用对象

  • 使用 Replace Constructor with Factory Method
  • 编译,测试
  • 决定由什么对象负责提供访问新对象的途径(可能是一个静态字典或一个注册表对象;也可以使用多个对象作为新对象的访问点)
  • 决定这些引用对象应该预先创建号,或是应该动态创建(如果这些引用对象是预先创建号的,而你必须从内存中将它们读取出来,那么就得去报它们在被需要的时候能够被及时加载)
  • 修改工厂函数,令它返回引用对象(如果对象是预先创建号的,你就需要考虑:万一有人请求一个并不存在的对象,要如何处理错误;可能希望对工厂函数使用 Rename Method,使其传达这样的信息:它返回的是一个已存在的对象)
  • 编译,测试

将引用对象改为值对象 Change Reference to Value

你有一个引用对象,很小且不可变,而且不易管理 -> 将它变成一个值对象

  • 检查重构目标是否为不可变对象,或是否可修改为不可变对象(如果该对象目前还不是不可变的,就使用 Removing Setting Method,直到它成为不可变的为止;如果无法将该对象修改为不可变的,就放弃使用本项重构)
  • 建立 equals()hashCode()
  • 编译,测试
  • 考虑是否可以删除工厂函数,并将构造函数声明为 public

以对象取代数组 Replace Array with Object

你有一个数组,其中的元素各自代表不同的东西 -> 以对象替换数组。对于数组中的每个元素,以一个字段来表示

  • 新建一个类表示数组所拥有的信息,并在其中以一个 public 字段保存原先的数组
  • 修改数组的所有用户,让它们改用新类的实例
  • 编译,测试
  • 逐一为数组元素添加取值/设值函数。根据元素的用途,为这些访问函数命名。修改客户代码,让它们动过访问函数取用数组内的元素。每次修改后,编译并测试
  • 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为 private
  • 编译
  • 对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改用上述的新建字段
  • 每修改一个元素,编译并测试
  • 数组的所有元素都有了相应字段之后,删除该数组

复制『被监视的数据』 Duplicate Observed Data

你有一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据 -> 将该数据复制到一个领域对象中。建立一个 Observer 模式,用以同步领域对象和 GUI 对象内的重复数据

  • 修改展现类,使其成为领域类的 Observer[GOF](如果尚未有领域类,就建立一个;如果没有『从展现类到领域类』的关联,就将领域类保存与展现类的一个字段中)
  • 针对 GUI 类中的领域数据,使用 Self Encapsulate Field
  • 编译,测试
  • 在事件处理函数中调用设值函数,直接更新 GUI 组件
  • 编译,测试
  • 在领域类中定义数据及其相关访问函数(确保领域类中的设值函数能够触发 Observer 模式的通报机制;对于被观察的数据,在领域类中使用与展现类所用的相同类型来保存。后续重构中你可以自由改变这个数据类型)
  • 修改展现类中的访问函数,将它们的操作对象改为领域对象(而非 GUI 组件)
  • 修改 Observer 的 update(),使其从相应的领域对象中将所需数据复制给 GUI 组件
  • 编译,测试

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

两个类都需要使用对方特性,但期间只有一条单向连接 -> 添加一个反向指针,并使修改函数能够同时更新两条连接

  • 在被引用类中增加一个字段,用以保存反向指针
  • 决定由哪个类——引用端还是被引用端——控制关联关系
  • 在被控端建立一个辅助函数,其命名应该清楚指出它的有限用途
  • 如果既有的修改函数在控制端,让它负责更新方向指针
  • 如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数

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

两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性 -> 去除不必要的关联

双向关联很有用,但是也必须为它付出代价,那就是维护双向连接、确保对象被正确创建和删除增加的复杂度。而且,由于很多程序员并不习惯使用双向关联,它往往成为错误之源。大量的双向连接也很容易造成『僵尸对象』:某个对象本来已经该死亡了,却仍然保留在系统中,因为对它的引用还没有完全清除。

  • 找出保存『你想去除的指针』的字段,检查它的每一个用户,判断是否可以去除该指针(不但要检查直接访问点,也要检查调用这些直接访问点的函数)
  • 如果客户使用了取值函数,先运用 Self Encapsulate Field 将待删除字段自我封装起来,然后使用 Substitute Algorithm 对付取值函数,令它不再使用该字段。然后编译、测试
  • 如果客户并未使用取值函数,那就直接修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象。每次修改后,编译并测试
  • 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段
  • 编译,测试

以字面常量取代魔法数 Replace Magic Number with Symbolic Constant

你有一个字面数值,带有特别含义 -> 创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量

  • 声明一个常量,令其值为原本的魔法数值
  • 找出这个魔法数的所有引用点
  • 检查是否可以使用这个新声明的常量来替换该魔法数。如果可以,便以此常量替换之
  • 编译
  • 所有魔法数都被替换完毕后,编译并测试。此时整个程序应该运转如常

封装字段 Encapsulate Field

你的类中存在一个 public 字段 -> 将它声明为 private,并提供相应的访问函数

  • 为 Public 字段提供取值/设值函数
  • 找到这个类以外使用该字段的所有地点。用取值/设置函数进行替代
  • 每次修改之后,编译并测试
  • 将字段的所有用户修改完毕后,把字段声明为 private
  • 编译,测试

封装集合 Encapsulate Collection

有个函数返回一个集合 -> 让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数(类似 MVC 的 M)

  • 加入为集合添加/移除元素的函数
  • 将保存集合的字段初始化为一个空集合
  • 编译
  • 找出集合设值函数的所有调用者。你可以修改那个设值函数,让它使用上述新建立的『添加/移除元素』函数;也可以直接修改调用端,改让它们调用上述新建立的『添加/移除元素』函数
  • 编译,测试
  • 找出所有『通过取值函数获得集合并修改其内容』的函数。逐一修改这些函数,让它们改用添加/移除函数。每次修改后,编译并测试
  • 修改完上述所有『通过取值函数获得集合并修改集合内容』的函数后,修改取值函数自身,使它返回该集合的一个只读副本
  • 编译,测试
  • 找出取值函数的所有用户,从中找出应该存在于集合所属对象内的代码。运用 Extract Method 和 Move Method 将这些代码移到宿主对象去
  • 修改现有取值函数的名字,然后添加一个新取值函数,使其返回一个枚举。找出旧取值函数的所有被实用点,将它们都改为使用新取值函数
  • 如果这一步跨度太大,可以先使用 Rename Method 修改原取值函数的名称;再建立一个新取值函数用以返回枚举;最后再修改所有调用者,使其调用新取值函数
  • 编译,测试

以数据类取代记录 Replace Record with Data Class

你需要面对传统编程环境中的记录结构 -> 为该记录创建一个『哑』数据对象

  • 新建一个类,表示这个记录
  • 对于记录中的每一项数据,在新建的类中建立对应的一个 private 字段,并提供相应的取值/设值函数

以类取代类型码 Replace Type Code with Class

  • 为类型码建立一个类
  • 修改源类的实现,让它使用上述新建的类
  • 编译,测试
  • 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类
  • 逐一修改源类用户,让它们使用新接口
  • 每修改一个用户,编译并测试
  • 删除使用类型码的旧接口,并删除保存旧类型码的静态变量
  • 编译,测试

以子类取代类型码 Replace Type Code with Subclasses

你有一个不可变的类型码,它会影响类的行为 -> 以子类取代这个类型码

  • 使用 Self Encapsulate Field 将类型码自我封装起来(如果类型码被传递给构造函数,就需要将构造函数换成工厂函数)
  • 为类型码的每一个数值建立一个相应的子类。在每个子类中覆写类型码的取值函数,使其返回相应的类型码值
  • 每建立一个新的子类,编译并测试
  • 从超类中删掉保存类型码的字段。将类型码访问函数声明为抽象函数
  • 编译,测试

以 State/Strategy 取代类型码 Replace Type Code with State/Strategy

你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它

  • 使用 Self Encapsulate Field 将类型码自我封装起来
  • 新建一个类,根据类型码的用途为它命名。这就是一个状态对象
  • 为这个新类添加子类,每个子类对应一种类型码
  • 在超类中建立一个抽象的查询函数,用以返回类型码。在每个子类中覆写该函数,返回确切的类型码
  • 编译
  • 在源类中建立一个字段,用以保存新建的状态对象
  • 调整源类中负责查询类型码的函数,将查询动作转发给状态对象
  • 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给『保存状态对象』的那个字段
  • 编译,测试

以字段取代子类 Replace Subclass with Fields

你的各个子类的唯一差别只在『返回常量数据』的函数身上 -> 修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类

  • 对所有子类使用 Replace Constructor with Factory Method
  • 如果有任何代码直接引用子类,令它改而引用超类
  • 针对每个常量函数,在超类中声明一个 final 字段
  • 为超类声明一个 protected 构造函数,用以初始化这些新增字段
  • 新建或修改子类构造函数,使他调用超类的新增构造函数
  • 编译,测试
  • 在超类中实现所有的常量函数,令它们返回相应字段值,然后将该函数从子类中删掉
  • 每删除一个常量函数,编译并测试
  • 子类中所有的常量函数都被删除后,使用 Inline Method 将子类构造函数内联到超类的工厂函数中
  • 编译,测试
  • 将子类删掉
  • 编译,测试
  • 重复『内联构造函数、删除子类』过程,直到所有子类都被删除

简化条件表达式

相比于面向过程程序,免息那个对象程序的条件表达式通常比较少,这是因为很多条件行为都被多态机制处理掉了。多态还有一种十分有用但鲜为人知的用途:通过 Introduce Null Object 去除对于 null 值的检验。

分解条件表达式 Decompose Conditional

你有一个复杂的条件(if-then-else)语句 -> 从 if, then, else 三个段落中分别提炼出独立函数

  • 将 if 段落提炼出来,构成一个独立函数
  • 将 then 段落和 else 段落都提炼出来,各自构成一个独立函数

合并条件表达式 Consolidate Conditional Expression

你有一系列条件测试,都得到相同结果 -> 将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数

  • 确定这些条件语句都没有副作用(如果条件表达式有副作用,你就不能使用本项重构)
  • 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
  • 编译,测试
  • 对合并后的表达式实施 Extract Method

合并重复的条件片段 Consolidate Duplicate Conditional Fragments

在条件表达式的每个分支上有着相同的一段代码 -> 将折断代码搬移到条件表达式之外

  • 鉴别出『执行方式不随条件变化而变化』的代码
  • 如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前
  • 如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后
  • 如果这些共同代码位于条件表达式中段,就需要观察来向前或向后移动
  • 如果共通代码不止一条语句,应该先使用 Extract Method 将共通代码提炼到一个独立函数中,再以前面所说的办法来处理

移除控制标记 Remove Control Flag

在一系列布尔表达式中,某个变量带有『控制标记(control flag)』的作用 -> 以 break 语句或 return 语句取代控制标记

  • 找出让你跳出这段逻辑的控制标记值
  • 找出对标记变量赋值的语句,代以恰当的 break 语句或 continue 语句
  • 每次替换后,编译并测试

在未能提供 break 和 continue 语句的编程语言中,可以使用下述办法

  • 运用 Extract Method,将整段逻辑提炼到一个独立函数中
  • 找出让你跳出这段逻辑的控制标记值
  • 找出对标记变量赋值的语句,代以恰当的 return 语句
  • 每次替换后,编译并测试

以 Guard 语句取代嵌套条件表达式 Replace Nested Conditional with Guard Clauses

函数中的条件逻辑使人难以看清正常的执行路径 -> 使用 Guard 语句表现所有特殊情况

  • 对于每个检查,放进一个 Guard 语句(要么从函数中返回,要么抛出一个异常)
  • 每次将条件检查替换成 Guard 语句后,编译并测试(如果所有 Guard 语句都导致相同结果,请使用 Consolidate Conditional Expressions)

以多态取代条件表达式 Replace Conditional with Polymorphism

你手上有个条件表达式,它根据对象类型的不同而选择不同的行为 -> 将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数

  • 如果要处理的表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用 Extract Method 将它提炼到一个独立函数去
  • 如果有必要,使用 Move Method 将条件表达式放置到继承结构的顶端
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
  • 编译,测试
  • 在超类中删掉条件表达式内被复制了的分支
  • 编译,测试
  • 针对条件表达式的每个分支,重复上述过程,直到所有分支都被移到子类内的函数为止
  • 将超类之中容纳条件表达式的函数声明为抽象函数

引入 Null 对象

你需要再三检查某对象是否为 null -> 将 null 值替换为 null 对象

  • 为源类建立一个子类,使其行为就像是源类的 null 版本。在源类和 null 子类中都加上 isNull() 函数,前者的 isNull() 应该返回 false,后者的返回 true
  • 编译
  • 找出所有『请求源对象却获得一个 null』 的地方,修改这些地方,使它们改而获得一个空对象
  • 找出所有『将源对象与 null 做比较的地方』,修改这些地方,使它们调用 isNull() 函数
  • 编译,测试
  • 找出这样的程序点:如果对象不是 null,做 A 动作,否则做 B 动作
  • 对于每一个上述抵挡,在 null 类中覆写 A 动作,使其行为和 B 动作相同
  • 使用上述被覆写的动作,然后删除『对象是否等于 null』 的条件测试。编译并测试

引入断言 Introduce Assertion

某一段代码需要对程序状态做出某种假设 -> 以断言明确表现出这种假设

  • 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况

简化函数调用

容易被理解和被使用的接口,是开发良好面向对象软件的关键。良好的接口只像用户展现必须展现的东西。如果一个接口暴露了过多细节,你可以将不必要暴露的东西隐藏起来,从而改进接口的质量。

函数改名 Rename Method

函数名称未能揭示函数的用途 -> 修改函数名称

  • 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,将它命名为你想要的新名称。将旧函数的代码复制到新函数中,并进行适当调整
  • 编译
  • 修改旧函数,令它将调用转发给新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,修改它们,令它们改而引用心函数。每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

添加参数 Add Parameter

某个函数需要从调用端得到更多信息 -> 为此函数添加一个对象函数,让该对象带进函数所需信息

  • 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,名称与原函数同,只是加上新添参数,将旧代码复制到新函数中
  • 编译
  • 修改旧函数,令它调用新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,修改它们,令它们改而引用心函数。每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

移除参数 Remove Parameter

函数本体不再需要某个参数 -> 将该参数去除

  • 检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤
  • 声明一个新函数,名称与原函数同,只是去除新添参数,将旧代码复制到新函数中
  • 编译
  • 修改旧函数,令它调用新函数
  • 编译,测试
  • 找出旧函数的所有被引用点,修改它们,令它们改而引用心函数。每次修改后,编译并测试
  • 删除旧函数
  • 编译,测试

将查询函数和修改函数分裂 Separate Query from Modifier

某个函数既返回对象状态值,又修改对象状态 -> 建立两个不同的函数,其中一个负责查询,另一个负责修改

  • 新建一个查询函数,令它返回的值与原函数相同
  • 修改原函数,令它调用查询函数,并返回获得的结果
  • 编译,测试
  • 将调用原函数的代码改为调用查询函数。然后在调用查询函数的那一行之前,加上对原函数的调用。每次修改后,编译并测试
  • 将原函数的返回值改为 void,并且删掉其中所有的 return 语句

令函数携带参数 Parameterized Method

若干函数做了类似的工作,但在函数本体中却包含了不同的值 -> 建立单一函数,以参数表达那些不同的值

  • 新建一个带有参数的函数,使他可以先替换先前所有的重复性函数
  • 编译
  • 将调用旧函数的代码改为调用新函数
  • 编译,测试
  • 对所有旧函数重复上述步骤,每次替换后,修改并测试

以明确函数取代参数 Replace Parameter with Explicit Methods

你有一个函数,其中完全取决于参数值而采取不同行为 -> 应该针对该参数的每一个可能值,建立一个独立函数

  • 针对参数的每一种可能值,新建一个明确函数
  • 修改条件表达式的每个分支,使其调用合适的新函数
  • 修改每个分支后,编译并测试
  • 修改原函数的每一个被调用点,改而调用上述的某个合适的新函数
  • 编译,测试
  • 所有调用端都修改完毕后,删除原函数

保持对象完整 Preserve Whole Object

你从某个对象中取出若干值,将它们作为某一次函数调用时的参数 -> 改为传递整个对象

  • 对你的目标函数新添一个参数项,用以代表原数据所在的完整对象
  • 编译,测试
  • 判断哪些参数可被包含在新添的完整对象中
  • 选择上述参数之一,将被调用函数中原来引用该参数的地方,改为调用新添参数对象的相应取值函数
  • 删除该项参数
  • 编译,测试
  • 针对所有可从完整对象中获得的参数,重复上述过程
  • 删除调用端中那些带有被删除参数的代码
  • 编译,测试

以函数取代参数 Replace Parameter with Method

对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数 -> 让参数接受者去除该项参数,并直接调用前一个函数

  • 如果有必要,将参数的计算过程提炼到一个独立函数中。将函数本体内引用该参数的地方改为调用新建的函数
  • 每次替换后,修改并测试
  • 全部替换完成后,使用 Remove Parameter 将该参数去掉

引入参数对象 Introduce Parameter Object

某些参数总是很自然地同时出现 -> 以一个对象取代这些参数

  • 新建一个类,用以表现你想替换的一组参数。将这个类设为不可变的
  • 编译
  • 针对使用该组参数的所有函数,实施 Add Parameter,传入上述新建类的实例对象,并将次参数设为 null
  • 对于 Data Clumps 中的每一项(在此均为参数),从函数签名中移除,并修改调用端和函数本体,令它们都改而通过新的参数对象取得该值
  • 每去除一个参数,编译并测试
  • 将原先的参数全部去除之后,观察有无适当函数可以运行 Move Method 搬移到参数对象之中

移除设值函数 Remove Setting Method

类中的某个字段应该在对象创建时被设值,然后就不再改变 -> 去掉该字段的所有设值函数

  • 检查设值函数被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用
  • 修改构造函数,使其直接访问设值函数所针对的那个变量
  • 编译,测试
  • 移除这个设值函数,将它所针对的字段设为 final
  • 编译,测试

隐藏函数 Hide Method

有一个函数,从来没有被其他任何类用到 -> 将这个函数修改为 private

  • 经常检查有没有可能降低某个函数的可见度
  • 尽可能降低所有函数的可见度
  • 每完成一组函数的隐藏之后,编译并测试

以工厂函数取代构造函数 Replace Constructor with Factory Method

你希望在创建对象时不仅仅是做简单的构建动作 -> 将构造函数替换为工厂函数

  • 新建一个工厂函数,让它调用现有的构造函数
  • 将调用构造函数的代码改为调用工厂函数
  • 每次替换后,编译并测试
  • 将构造函数声明为 private
  • 编译

封装向下转型 Encapsulate Downcast

某个函数返回的对象,需要由函数调用者执行向下转型(downcast)

  • 找出必须对函数调用结果进行向下转型的地方(这种情况通常出现在返回一个集合或迭代器的函数中)
  • 将向下转型动作搬移到该函数中(针对返回集合的函数,使用 Encapsulate Collection)

以异常取代错误码 Replace Error Code with Exception

某个函数返回一个特定的代码,用以表示各种错误情况 -> 改用异常

  • 决定应该抛出受控(checked)异常还是非受控(unchecked)异常
  • 找到该函数的所有者,对它们进行相应调整,让他们使用异常
  • 修改该函数的签名,令它反映出新用法

以测试取代异常 Replace Exception with Test

面对一个调用者可以预先检查的条件,你抛出了一个异常 -> 修改调用者,使它在调用函数之前先做检查

  • 在函数调用点之前,放置一个测试语句,将函数内 catch 区段中的代码复制到测试句的适当 if 分支中
  • 在 catch 区段起始处加入一个断言,确保 catch 区段绝对不会被执行
  • 编译,测试
  • 移除所有 catch 区段,让后将 try 区段内的代码复制到 try 之外,然后移除 try 区段
  • 编译,测试

处理概括关系

字段上移 Pull Up Field

两个子类拥有相同的字段 -> 将该字段移至超类

  • 针对提升字段,检查它们所有被使用点,确认它们以同样的方式被使用
  • 如果这些字段的名称不同,先将它们改名,使每一个名称都和你想为超类字段取的名称相同
  • 编译,测试
  • 在超类中新建一个字段(声明为 protected)
  • 移除子类中的字段
  • 编译,测试
  • 考虑对超类的新建字段使用 Self Encapsulate Field

函数上移 Pull Up Method

有些函数,在各个子类中产生完全相同的结果 -> 将函数移至超类

  • 检查待提升函数,确定它们是完全一致的
  • 如果待提升函数的签名不同,将那些签名都修改为你想要在超类中使用的签名
  • 在超类中新建一个函数,将某个待提升函数的代码复制到其中,做适当调整,然后编译
  • 移除一个待提升的子类函数
  • 编译,测试
  • 逐一移除待提升的子类函数,直到只剩下超类中的函数为止。每次移除之后都需要测试
  • 观察该函数的 调用者,看看是否可以改为使用超类类型的对象

构造函数本体上移 Pull Up Constructor Body

你在各子类中拥有一些构造函数,它们的本体几乎完全一致 -> 在超类中新建一个构造函数,并在子类构造函数中调用它

  • 在超类中定义一个构造函数
  • 将子类构造函数中的共同代码搬移到超类构造函数中
  • 将子类构造函数中的共同代码删掉,改为调用新建的超类构造函数
  • 编译,测试

函数下移 Push Down Method

超类中的某个函数只与部分(而非全部)子类有关 -> 将这个函数移到相关的那些子类去

  • 在所有子类声明该函数,将超类中的函数本体复制到每一个子类函数中
  • 删除超类中的函数
  • 编译,测试
  • 将该函数从所有不需要它的那些子类中删掉
  • 编译,测试

字段下移 Push Down Field

超类中的某个字段只部分(而非全部)子类用到 -> 将这个字段移到需要它的那些子类去

  • 在所有子类中声明该字段
  • 将该字段从超类中移除
  • 编译,测试
  • 将该字段从所有不需要它的那些子类中删掉
  • 编译,测试

提炼子类 Extract Subclass

类中的某些特性只被某些(而非全部)实例用到 -> 新建一个子类,将上面所说的那一部分特性移到子类中

  • 为源类定义一个新的子类
  • 为这个新的子类提供构造函数
  • 找出调用超类构造函数的所有地点。如果它们需要的是新建的子类,令它们改而调用新构造函数
  • 逐一使用 Push Down Method 和 Push Down Field 将源类的特性移到子类去
  • 找到所有这样的字段:它们所传达的信息如今可由继承体系自身传达。以 Self Encapsulate Field 避免直接使用这些字段,然后将它们的取值函数替换为多态常量函数。所有使用这些字段的地方都应该以 Replace Conditional with Polymorphism 重头
  • 每次下移之后,编译并测试

提炼超类 Extract Superclass

两个类具有相似特性 -> 为这两个类建立一个超类,将相同特性移至超类

  • 为原本的类新建一个空白的抽象超类
  • 运用 Pull Up Field, Pull Up Method 和 Pull Up Constructor Body 逐一将子类的共同元素上移到超类
  • 每次上移后,编译并测试
  • 检查留在子类中的函数,看它们是否还有共通成分
  • 将所有共同元素都上移到超类之后,检查子类的所有用户。如果它们只使用共同接口,你就可以把它们请求的对象类型改为超类

提炼接口 Extract Interface

  • 新建一个空接口
  • 在接口中声明待提炼类的共同操作
  • 让相关的类实现上述接口
  • 调整客户端的类型声明,令其使用该接口

折叠继承体系 Collapse Hierarchy

超类和子类之间无太大区别 -> 将它们合为一题

  • 选择你想移除的类:是超类还是子类?
  • 把想要移除的类的所有行为和数据搬移到另一个类
  • 每次移动后,编译并测试
  • 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。这个动作将会影响变量的声明、参数的类型以及构造函数
  • 移除我们的目标
  • 编译,测试

塑造模板函数 Form Template Method

你有一些子类,其中响应的某些函数以相同顺序执行类似的操作,但各个操作在细节上有所不同 -> 将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同。然后将原函数上移至超类

  • 在各个子类中分解目标函数,使分解后的各个函数要不完全相同,要不完全不同
  • 运用 Pull Up Method 将各子类内完全相同的函数上移至超类
  • 对于那些完全不同的函数,实施 Rename Method,使所有这些函数的签名完全相同
  • 修改上述所有签名后,编译并测试
  • 运用 Pull Up Method 将所有原函数逐一上移至超类。在超类中将那些代表各种不同操作的函数定义为抽象函数
  • 编译,测试
  • 移除其他子类中的原函数,每删除一个,编译并测试

以委托取代继承 Replace Inheritance with Delegation

某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据 -> 在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系

  • 在子类中新建一个字段,使其引用超类的一个实例,并将它初始化为 this
  • 修改子类内的所有函数,让它们不再使用超类,转而使用上述俺哥委托字段。每次修改后,编译并测试
  • 去除两个类之间的继承关系,新建一个受托类的对象赋给受托字段
  • 针对客户端所用的每一个超类函数,为它添加一个简单的委托函数
  • 编译,测试

以继承取代委托 Replace Delegation with Inheritance

你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数 -> 让委托类继承受托类

  • 让委托端成为受托端的一个子类
  • 编译
  • 将受托字段设为该字段所处对象本身
  • 去掉简单的委托函数
  • 编译并测试
  • 将所有其他涉及委托关系的代码,改为调用对象自身
  • 移除受托字段

大型重构

耗费相当长时间,只在需要添加新功能或修补错误时才进行重构。这一章不写具体做法,很多时候需要灵活变通

梳理并分解继承体系 Tease Apart Inheritance

某个继承体系同时承担两项责任 -> 建立两个继承体系,并通过委托关系让其中一个可以调用另一个

将过程化设计转化为对象设计 Convert Procedural Design to Objects

你手上有一些传统过程化风格的代码 -> 将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中

将领域和展示分离 Separate Domain from Presentation

某些 GUI 类之中包含了领域逻辑 -> 将领域逻辑分离出来,为它们建立独立的领域类

提炼继承体系 Extract Hierarchy

你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的 -> 建立继承体系,以一个子类表示一种特殊情况

捧个钱场?

热评文章