重构——在对象之间搬移特性

Move Method(搬移函数)

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

动机

如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就使用搬移函数,通过这种手段,可以使系统中的类更简单。

做法

  1. 检查源类中被源函数所使用的的一切特性,考虑它们是否也该被搬移;

  2. 检查源类的子类和超类,看看是否有该函数的其他声明;

  3. 在目标类中声明这个函数;

  4. 将源函数的代码复制到目标函数中,调整后者,使其等在新家中正常运行;

  5. 决定如何从源函数正确引用目标对象;

  6. 修改源函数,使之成为一个纯委托函数;

  7. 决定是否删除源函数,或将它当做一个委托函数保留;

  8. 如果要移除源函数,请将源类中与源函数的所有调用替换为对目标函数的调用;

当时用源类的特性时,有四种选择:将这个特性也移到目标类、建立或使用一个从目标类到源类的引用关系、将源对象当做参数传给目标函数、如果所需特性是个变量,将它当做参数传给目标函数。

如果需要源类的多个特性,那么就将源对象传递给目标函数,但是如果目标含糊需要太多的源类特性,就得进一步重构,通常这种情况下需要分解目标函数,并将其一部分移回源类。

Move Field(搬移字段)

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

动机

在类之间移动状态和行为,室重构过程中必不可少的措施,如果对一个字段,在其所驻类之外的另一个类有更多函数使用了它,则需要考虑搬移这个字段。所谓“使用”可能是通过设值、取值函数简介进行的。

做法

  1. 如果字段的访问级是public,使用封装属性将它封装起来;

  2. 在目标类中建立与源字段相同的字段,并同时建立相应的设值/取值函数;

  3. 决定如果在源对象中引用目标对象;

首先看是否有现成的字段或函数可以帮助你得到目标对象,如果没有,就看能否请以建立这样一个函数。如果还不行,就得在源类中新建一个字段来存放目标对象。

  1. 删除源字段;

  2. 将所有对源字段的引用替换为对某个目标函数的调用;

如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用;如果要对该变量赋值,就把源字段的引用替换成对设值函数的调用。
如果源字段不是private的,就必须在源类的所有子类中查找源字段的引用点,并进行相应替换。

Extract Class (提炼类)

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

重构——在对象之间搬移特性

动机

一个类应该是一个清楚的抽象,处理一些明确的责任。但是在实际中随着业务功能的增加,类会不断成长扩展。给某个类添加一项新责任时,会觉得不值得为这项责任分离出一个单独的类。

这样的类往往含有大量函数和数据,导致往往太大而不易理解。此时需要考虑哪些部分可以分离出去,并将它们分离到一个单独的类中。分离的原则就是如果你搬移了某些字段和函数,会发生什么事,其他字段和函数是否因此变得无意义。

另一个往往在开发后期出现的信号是类的子类化方式,如果发现子类化只影响类的部分特性,或如果发现某些特性需要以一种方式来子类化,某些特性则需要以另一种方式来子类化,这就意味着你需要分解原来的类。

做法

  1. 决定如何分解类所负的责任;

  2. 建立一个新类,用以表现从旧类中分离出来的责任;

  3. 建立“从旧类访问新类”的连接关系;

  4. 对于你想搬移的每一个字段,运行搬移特性搬移;

  5. 使用搬移函数将必要函数搬移到新类,先搬移较底层函数,再搬移较高层函数;

  6. 决定是否公开新类,如果需要公开,就要决定让它成为引用对象还是不可变的值对象。

Inline Class(将类内联化)

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

动机

如果一个类不在承担足够责任、不再有单独存在的理由,我们就会挑选这一“萎缩类”的最频繁用户然后将“萎缩类”塞进另一个类中。

Hide Delegate(隐藏“委托关系”)

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

动机

封装即使不是对象最关键的特征,也是特征之一,封装意味着每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的对象就会比较少。

如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知道这层委托关系。万一委托关系发生变化,客户也得相应变化,可以再服务对象上放置一个简单的委托函数,将委托关系隐藏起来,而从去除这种依赖。

做法

  1. 对于每一个委托关系中的函数,在服务对象端建立一个简单地委托函数;

  2. 调整客户,令它只调用服务对象提供的函数;

  3. 如果将来不再有任何客户需要取用受托类,便可移除服务对象中的相关访问函数。

Remove Middle Man(移除中间人)

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

“封装受托对象”虽然可以解耦,但是这层封装也是要付出代价的,代价就是:每当客户要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性越来越多,服务类完全变成了一个中间人,此时就应该让客户端直接调用受托类。

合适的隐藏程度这个尺度在不断改变。

Introduce Foreign Method(引入外加函数)

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

重构——在对象之间搬移特性

动机

如果一个类不能提供你目前需要的服务,并且不能修改该类源码,就得在客户端进行编码,不足你需要的那个函数。当你需要以外加函数实现一项功能最明确的信号就是这个函数原本应该在提供服务的类中实现。

但是如果你发现自己为一个服务类建立了大量外加函数,或者发现有许多类都需要同样的外加函数,就不应该再使用本重构,而应该使用引入本地扩展

Introduce Local Extension(引入本地扩展)

你需要为服务类提供一些额外函数,但你无法修改这个类。

建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类。

动机

如果你只需要一两个外加函数,可以使用引入外加函数,但是如果你需要的额外函数超过两个,外加函数就很难控制它们。所以你需要将这些函数组织在一起,放到一个恰当的地方去。

要达到这一目的,两种标准对象技术——子类化和包装是显而易见的方法,统称为本地扩展

所谓本地扩展,它提供源类的一切特性,同时额外添加新特性,在任何使用源类的地方们都可以使用本地扩展取而代之。

在子类和包装类之间做选择时,通常首选子类,因为这样的工作量少。但是子类化方案还必须产生一个子类对象,这种情况下,如果有其他对象引用了旧对象,我们就同时有两个对象保存元数据。如果原数据是不可修改的,则可以放心复制。但如果原数据允许被修改,问题来了,因为一个修改动作无法同时改变两份副本。这时就必须改用包装类。

做法

  1. 建立一个扩展类,将它作为原始类的子类和包装类;
  2. 在扩展类中加入转型构造函数;

所谓转型构造函数是指接收原对象作为参数的构造函数。
如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数。
如果采用包装类方案,那么转型构造函数应该将它得到的转入参数以实例变量的形式保存起来,用作接收委托的原对象。

  1. 在扩展类中加入新特性;
  2. 根据需要,将原对象替换为扩展对象;
  3. 将针对原始类定义的所有外加函数搬移到扩展类中。