重构-改善既有代码的设计:对象之间移动特性的八种方法(五)

2022-04-14 19:51:36 浏览数 (2)

在面向对象编程过程中,明确该对象的职责。类应该是:做自己该做的事,应尽该尽的义务,

1.Move Method 移动函数

类的行为做到单一职责 不要越俎代庖: 你的程序中,有个函数与其所驻类之外的另一个类进行更多的交流:调用后者,或被后者调用。 在该函数最常用引用的类中建立一个有着类似行为的新函数。将旧函数编程一个单纯的委托函数,或是将旧函数完全移除。

“搬移函数”是重构理论的支柱。如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就需要搬移函数。通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落的实现系统交付的工作。 浏览类的所有函数,从中找出这样的函数:使用另一个对象的次数比使用自己所驻对象的次数还多。一旦移动了一些字段,就该做这样的检查。一旦发现有可能搬移的函数,就观察调用它的那一端、它调用的那一端,已经继承体系中它的任何一个重定义函数。然后,会根据“这个函数与哪个对象的交流比较多”,决定其移动路径。 这往往不是容易做出的决定。如果不能肯定是否应该移动一个函数,就继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。有时候,即使你移动了其他函数,还是很难对眼下这个函数做出决定。其实这也没什么大不了的。如果真的很难做出决定,那么也许“移动这个函数与否”并不是那么重要。所以,就凭本能去做,反正以后总是可以修改的。

2.Move Field 搬移字段

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

在类之间移动状态和行为,是重构过程中必不可少的措施。随着系统发展,你会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。在这个星期看似合理而正确的设计决策,到了下个星期可能不再正确。这没问题,如果你从来没遇到这种情况,那才有问题。

如果发现对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就考虑搬移这个字段。上述所谓“使用”可能是通过设值/取值函数间接进行的。也可能移动该字段的用户(某个函数),这取决于是否需要保持接口不受变化。如果这些函数看上去很适合待在原地,就选择搬移字段。

使用Extract Class (提炼类)时,也可能需要搬移字段。此时可以先搬移字段,然后搬移函数。

3.Extract Class提炼类

一个类应该是一个清楚地抽象,处理一些明确的责任,把大类分解成小类:某个类做了应该由两个类做的事。建立一个新类,将相关的字段和函数从旧类搬移到新类。

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

4. Inline Class 将类内联化

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

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

5. Hide Delegate 隐藏委托关系

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

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

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

对于某些或全部客户,你可能会发现,有必要先使用Extract Class (提炼类)。一旦你对所有客户都隐藏了委托关系,就不再需要在服务对象的接口中公开被委托对象。

6.Remove middle Man 移除中间人

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

在Hide Delegate (隐藏委托关系)的“动机”中,谈到了“封装委托对象”的好处。但是这层封装也是要付出代价的,它的代价是:每当客户要使用受托类的新特性时,你就必须在服务端添加一个简单委托函数。随着委托类的特性(功能)越来越多,这一过程让你痛苦不已。服务类完全变成了“中间人”,此时你就应该让客户直接调用受托类。

很难说什么程度的隐藏才是合适的。还好,有了Hide Delegate (隐藏委托关系)和Remove Middle Man (移除中间人),你大可不必操心这个问题。因为你可以在系统运行过程中不断进行调整。随着系统的变化,“合适的隐藏程度”这个尺度也相应改变。6个月前恰如其分的封装,现今可能就显得笨拙。重构的意义在于:你永远不必说对不起—只要把出问题的地方修补好就行了。

7. Introduce Foreign Method 引入外加函数

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

这种事情发生了太多次了,你正在使用一个类,它真的很好,为你提供了需要的所有服务。而后,你又需要一项新服务,这个类却无法供应。于是你开始咒骂“为什么不能做这件事?”如果可以修改源码,你便可以自行添加一个新函数;如果不能,你就得在客户端编码,补足你要的那个函数。

如果客户类只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个类。然而,如果你需要多次使用这个函数,就得不断重复这些代码。重复代码是软件万恶之源。这些重复代码应该被抽出来放进一个函数中。进行本项重构时,如果你以外加函数实现一项功能,那就是一个明确信号:这个函数原本应该在提供服务的类中实现。

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

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

8.Introduce Local Extension 引入本地扩展

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

类的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。如果你可能修改源码,最后的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用 Introduce Foreign Method (引入外加函数)。但如果你需要的额外函数超过2个,外加函数就很难控制它们了。所以你需要将这些函数组织在一起,放到一个恰当的地方去。要达到这个目的,2种标准对象技术—子类化(subclassing)和包装(wrapping)是显而易见的办法。这种情况下,把子类化和包装类统称为本地扩展。

所谓本地扩展是一个独立的类,但也是被扩展类的字类型:它提供源类的一切特性,同时额外添加新特性。在任何使用源类的地方,你都可以使用本地扩展取而代之。

使用本地扩展使你得以坚持:函数和数据应该被统一封装“的原则。如果你一直把本该放在扩展类中的代码零散的放置于其他类中,最终只会让其他这些类变得过分复杂,并使得其他函数难以被复用。

在子类和包装类之间做选择时,首选子类。因为这样的工作量比较少。制作子类的最大障碍在于,它必须在对象创建期实施。如果可以接管对象创建过程,那当然没问题;但如果你想在对象创建之后再使用本地扩展,就有问题了。此时,子类化方案还必须产生一个子类对象,这种情况下,如果有其他对象引用了旧对象,我们就同时有2个对象保存了原数据。如果原数据是不可修改的,那也没问题。可以放心进行复制;但如果原数据允许修改,问题就来了,因为一个修改动作无法同时改变2份副本。这时候就必须改用包装类。使用包装类时,对本地扩展的修改会波及原对象,反之亦然。

0 人点赞