重构方法_在对象之间搬移特性

概述

1、在对象的设计过程中,“决定把责任放在哪儿”即使不是最重要的事,也是最重要的事之一。
2、类往往会因为承担过多的责任而变得臃肿不堪。这种情况下,应该使用Extract Class 将一部分责任分离出去。如果一个类变得太“不负责任”,需要使用Inline Class(将类内联化)将它融入另一个类。如果一个类使用了另一个类,运用Hide Delegate(隐藏“委托关系”)将这种关系隐藏起来通常是有帮助的。有时候隐藏委托类会导致拥有者的接口经常变化,此时需要使用Remove Middle Man(移除中间人)。
3、Introduce Foreign Method(引入外加函数) 和 Introduce Local Extension(引入本地扩展)比较特殊。只有当不能访问某个类的源码时,却又想把其他责任移进这个不可修改的类时,才会使用这两个重构手法。如果想加入的是一个或两个函数,就会使用Introduce Foreign Method(引入外加函数);如果不止一两个函数,就使用Introduce Local Extension(引入本地扩展)。

重构手法介绍

1、Move Method(搬移函数)
动机

1、“搬移函数”是重构理论的支柱。如果一个类又太多的行为,或如果一个类与另一个类又太多合作而形成高度耦合,就应该使用搬移函数。通过这只种手段,可以使系统种的类更简单,这些类最终也将更干净利落地实现系统地交付任务。
2、浏览类的所有函数,从中寻找这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦我移动了一些字段,就该做这样的检查。一旦发现有可能移动的函数,我就会观察调用它的那一端、它调用的那一端,以及继承体系中它的任何一个重定义函数。然后,会根据“这个函数与哪个对象的交流比较多”,决定其移动路径。

做法

❑检查源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也该被搬移。
➾如果某个特性只被你打算搬移的那个函数用到,就应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候,搬移一组函数比逐一搬移简单些。

❑检查源类的子类和超类,看看是否有该函数的其他声明。
➾如果出现其他声明,你或许无法进行搬移,除非目标类也同样表现出多态性。

❑在目标类中声明这个函数。
➾你可以为此函数选择一个新名称——对目标类更有意义的名称。

❑将源函数的代码复制到目标函数中。调整后者,使其能在新家中正常运行。
➾如果目标函数使用了源类中的特性,你得决定如何从目标函数引用源对象。如果目标类中没有相应的引用机制,就把源对象的引用当作参数,传给新建立的目标函数。
➾如果源函数包含异常处理,你得判断逻辑上应该由哪个类来处理这一异常。如果应该由源类来负责,就把异常处理留在原地。

❑编译目标类。

❑决定如何从源函数正确引用目标对象。
➾可能会有一个现成的字段或函数帮助你取得目标对象。如果没有,就看能否轻松建立一个这样得函数。如果还是不行,就得在源类中新建一个字段来保存目标对象。这可能是一个永久性修改,但你也可以让它是暂时的,因为后继的其他重构项目可能会把这个新建字段去掉。

❑修改源函数,使之成为一个委托函数。

❑编译,测试。

❑决定是否删除源函数,或将它当作一个委托函数保留下来。
➾如果你经常要在源对象中应用目标函数,那么将源函数作为委托函数保留下来会比较简单。

❑如果要移除源函数,请将源类中对源函数的所有调用,替换为对目标函数的调用。
➾你可以每修改一个引用点就编译测试一次,也可以通过一次“查找/替换”改掉所有引用点,这通常简单一些。

❑编译,测试。

2、Move Field(搬移字段)
动机

1、在类之间移动状态和行为,是重构过程中必不可少的措施。随着系统发展,你会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。在这个星期看似合理而正确的设计决策,到了下个星期可能不再正确。这没问题,如果你从来没有遇到过这种情况,那才有问题。
2、如果我发现对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,我会考虑搬移这个字段。上述所谓“使用”可能是通过设值/取值函数间接进行的。我也可能移动该字段的用户(某个函数),这取决于是否需要保持接口不受变化。如果这些函数看上去很适合待在原地,我选择搬移字段。
3、使用Extract Class时,我也可能需要搬移字段。此时我会先搬移字段,然后再搬移函数。

做法

❑如果字段的访问级是public,使用Encapsulate Field将它封装起来。
➾如果你有可能移动那些频繁访问该字段的函数,或如果许多函数访问某个字段,先使用Self Encapsulate Field也许会有帮助。

❑编译,测试。

❑再目标类中建立与源字段相同的字段,并同时建立相应的设值/取值函数。

❑编译目标类。

❑决定如何在源对象中引用目标对象。
➾首先看是否有一个现成的字段或函数可以帮助你得到目标对象,如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在源类中建立一个字段来存放目标对象。这可能是个永久性修改,但你也可以让它是暂时的,因为后续重构可能会把这个新建字段删除掉。

❑删除源字段。

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

❑编译,测试。

3、EXtract Class(提炼类)
动机

1、你也许听说过类似这样的教诲:一个类应该是一个清楚的抽象,处理一些明确的责任。但是在实际工作中,类会不断成长拓展。你会在这儿加入一些功能,在那儿加入一些数据。给某个类添加一项责任时,你会觉得不值得为这项责任分离出一个单独的类。于是,随着责任不断增加,这个类会变得过分复杂。很快,你的类就会变得一团乱麻。
2、这样的类往往含有大量函数和数据。这样的类往往太大而不易理解。此时你需要考虑哪些部分可以分离出去,并将它们分离到一个单独的类中。如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去。一个有用的测试就是问自己,如果你搬移了某些字段和函数,会发生什么事?其他字段和函数是否因此变得无意义?
3、另一个往往在开发后期出现的信号是类的子类化方式。如果你发现子类化只影响类的部分特征,或如果你发现某些特性需要以一种方式来子类化,某些特性则需要以另一种方式子类化,这就意味你需要分解原来的类。

做法

❑决定如何分解类所负的责任。

❑建立一个新类,用以表现从旧类中分离出来的责任。
➾如果旧类剩下的责任与旧类名称不符,为旧类更名。

❑建立“从旧类访问新类”的连接关系。
➾有可能需要一个双向连接。但是在真正需要它之前,不要建立“从新类通往旧类”的连接。

❑对于你想搬移的每一个字段,运用Move Field搬移之。

❑每次搬移后,编译、测试。

❑使用Move Method将必要函数搬移到新类。先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函数”者),再搬移较高层函数。

❑每次搬移之后,编译、测试。

❑检查,精简每个类的接口。
➾如果你建立起双向连接,检查是否可以将它改为单向连接。

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

4、Inline Class(将类内联化)
动机

Inline Class正好与Extract Class相反。如果一个类不再承担足够责任、不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),我就会挑选这一“萎缩类”的最频繁用户(也是个类),以Inline Class手法将“萎缩类”塞进另一个类中。

做法

❑再目标类身上声明源类的public协议,并将其中所有函数委托至源类。
➾如果“以一个独立接口表示源类函数”更合适的话,就应该再内联之前先使用Extract Interface。

❑修改所有源类引用点,改而引用目标类。
➾将源类声明为private,以斩断包之外的所有引用可能。同时修改源类的名称,这便可使编译器帮助你捕捉到所有对于源类的隐藏引用点。

❑编译,测试。

❑运用Move Method和Move Field,将源类的特性全部搬移到目标类。

❑为源类举行一个简单的“丧礼”。

5、Hide Delegate(隐藏“委托关系”)
动机

1、”封装“即使不是对象的最关键特征,也是最关键特征之一。”封装“意味每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一变化的对象就会比较少——这会使变化比较容易进行。
2、任何学过对象技术的人都知道:虽然Java允许将字段声明为public,但你还是应该隐藏对象的字段。随着经验日渐丰富,你会发现,有更多可以(而且值得)封装的东西。
3、对于某些或全部客户,你可能会发现,有必要先使用Extract
Class。一旦你对所有客户都隐藏了委托关系,就不需要再服务对象的接口中公开被委托对象了。

做法

❑对于没一个委托关系中的函数,在服务对象端建立一个简单的委托函数。

❑调整客户,令它只调用服务对象提供的函数。
➾如果使用者和服务提供者不在同一个包,考虑修改委托函数的访问权限,让客户得以在包之外调用它。

❑每次调整后,编译并测试。

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

❑编译,测试。

6、Remove Middle Man(移除中间人)
动机

1、在Hide Delegate中,我谈到了”封装受托对象“的好处。但是这层封装也是要付出代价的,它的代价就是:每当客户要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着受托类的特性越来越多,这一过程会让你痛苦不已。服务类完全变成了一个”中间人“,此时你就应该考虑让客户直接调用受托类。
2、很难说什么程度的隐藏才是合适的。还好,有Hide Delegate和Remove Middle Man,你大可不必操心这个问题,因为你可以在系统运行过程中不断进行调整。随着系统的变化,”合适的隐藏程度“这个尺度也相应改变。6个月前恰如其分的封装,现今可能就显得笨拙。重构得意义就在于:你永远不必说对不起——只要把出问题得地方修补好就行了。

做法

❑建立一个函数,用以获得受托对象。

❑对于每个委托函数,在服务类中删除该函数,并让需要调用该函数得客户转为调用受托对象。

❑处理每个委托函数后,编译、测试。

7、Introduce Foreign Method(引入外加函数)
动机

1、这种事件发生过太多次了:你正在使用一个类,它真的很好,为你提供了需要的所有服务。而后,你又需要一项新服务,这个类却无法供应。于是你开始咒骂:”为什么不能做这件事?“如果可以修改源码,你便可以自行添加一个新函数;如果不能,你就得在客户端编码,补足你要的那个函数。
2、如果客户类只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个类。然而,如果你需要多次使用这个函数,就得不断重复这些代码。还记得吗,重复代码是软件万恶之源。这些重复代码应该被抽出来放进同一个函数中。进行本项重构时,如果你以外加函数实现一项功能,那就是一个明确信号:这个函数原本应该在提供服务类中实现。
3、如果你发现自己为一个服务类建立了大量外加函数,或者发现有许多类都需要同样的外加函数,就不应该再使用本项重构,而应该使用Introduce Local Extension。

做法

❑在客户类中建立一个函数,用来提供你需要的功能。
➾这个函数不应该调用客户类的任何特性。如果它需要一个值,把该值当作参数传给它。

❑以服务类实例作为该函数的第一个参数。

❑将该函数注释为:”外加函数(foreign method),应在服务类实现。“
➾这么一来,如果将来有机会将外加函数搬移到服务类中时,你便可以轻松找出这些外加函数。

8、Introduce Local Extension(引入本地扩展)
动机

1、很遗憾,类的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。如果你可以修改源码,最好的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用Introduce Foreign Method。但如果你需要的额外函数超过两个,外加函数就很难控制它们了。所以,你需要将这些函数组织在一起,放到一个恰当地方去。要达到这一目的,两种标准对象技术——子类化(subclassing)和包装(wrapping)——是显而易见的办法。这种情况下,我把子类或包装类统称为本地拓展(local extension)。
2、所谓本地拓展是一个独立的类,但是也是被拓展类的子类型:它提供类的一切特性,同时额外添加新特性。在任何使用源类的地方,你都可以使用本地拓展取而代之。
3、使用本地拓展使你得以坚持”函数和数据应该被统一封装“的原则。如果你一直把本该放在拓展类种的代码零散地放置在其他类中,最终只会让其他这些类变得过分复杂,并使得其中函数难以被复用。
4、在子类和包装类之间做选择时,我通常首选子类,因为这样工作量比较小。制作子类地最大障碍在于,它必须在对象创建期实施。如果我可以接管对象创建过程,那当然没问题;但如果你想在对象创建之后再使用本地拓展,就有问题了。此外,子类化方案还必须产生一个子类对象,这种情况下,如果有其他对象引用了旧对象,我们就同时有两个对象保存了原数据!如果原数据是不可修改地,那也没问题,我可以放心进行复制;但如果原数据允许被修改,问题就来了,因为一个修改动作无法同时改变两份副本。这时候我们就必须改用包装类。使用包装类时,对本地拓展地修改会波及原对象,反之亦然。

做法

❑建立一个拓展类,将它作为原始类地子类或包装类。

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

❑在拓展类中加入新特性。

❑根据需要,将原对象替换为拓展对象。

❑将针对原始类定义的所有外加函数搬移到拓展类中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值