重构—改善既有代码的设计7——在对象之间搬移特性

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

move field=》move method简单的移动对象行为

类往往因为承担过多的责任而变得臃肿不堪。extract class 将一部分责任分离出去。

一个类变得太“不负责任”,inline class将其融入另一个类

一个类使用了另一个类,hide delegate,将这种关系隐藏起来

隐藏委托类,导致拥有者的接口经常变化,remove middle man

当不能访问某个类的源码,又想把其他责任移进这个不可修改的类时,introduce foreign method、introduce local extension;想加入的只是1、2个函数,使用introduce foreign method;否则使用introduce local extension

1.move method:搬移函数

  有一个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用

  在该函数最常引用的类中,建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数,或将旧函数完全移除

  

  动机:

    move method:重构理论的支柱。如果一个类有太多行为;一个类与另一个类有太多合作而形成高度耦合

    可使系统中的类更简单,这些类最终也更干净利落地实现系统交付的任务

    迹象:

      使用另一个类的次数比使用自己所驻对象的次数还多

      一旦移动了一些字段,就该做这样的检查

      一旦发现有可能搬移的函数,就会观察调用它的那一段、它调用的那一段,以及继承体系中它的任何一个重定义函数

      根据“这个函数与哪个对象的交流比较多”,决定其移动路径

    如果不能肯定是否应该移动一个函数,就继续观察其他函数。移动其他函数往往会让这项决定变得容易一些。

    做法:

      检查源类中被源函数所使用的一切特性(包括字段、函数),考虑是否该被搬移

      如果某个特性另被多个函数使用了,可以考虑将使用该特性的所有函数全都一并搬移。有时候,搬移一组函数比逐一搬移简单些

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

      如果目标函数使用了源类中的特性,需要决定如何从目标函数引用源对象。如果目标类中没有相应的引用机制,将把源对象的引用当作参数,传递给新建立的目标函数

      如果源函数包含异常处理,需判断逻辑上应该由哪个类来处理这一异常。如果应该由源类来负责,就把异常处理留在原地

      决定如何重源函数正确引用目标对象。

        可能会有一个现成的字段、函数帮助取得目标对象。如果没有,则轻松建立一个这样的函数

        如果不行,在源类中新建一个字段来保存目标对象,可能是一个永久性的修改,也可以是暂时的。因为后继的其他重构可能将新建字段去掉

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

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

      每修改一个引用点,就编译测试一次,也可以通过一次“查找/替换”改掉所有引用点

      如果源类有多个特性,会将源对象传递给目标函数。如果目标函数需要太多源类特性,就需进一步重构。通常,会分解目标函数,并将其中一部分回源类

2.move feild:搬移字段

  某个字段被其所驻类之外的另一个类更多地用到

  在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段

  

  动机:

    在类之间移动状态、行为,是重构过程中必不可少的措施

    随着系统发展,会发现自己需要新的类,并需要将现有的工作责任拖到新的类中。在这个星期看似合理而正确的设计决策,到了下一个星期可能不再正确

    如果发现,对于一个字段,再其所驻类之外的另一个类中有更多函数使用了它,就会考虑搬移这个字段。

    “使用”:可能通过设置/取值函数间接进行的

    也可能移动该字段的用户(某个函数),这取决于是否需要保持接口不受变化。如果这些函数看上去很适合呆在原地,就只搬移字段

    extract class 使,也可能需要搬移字段。先搬移字段,再搬移函数

  做法:

    如果字段的访问级别使public,使用encapsulate field将其封装起来。

    如果有可能移动哪些频繁访问该字段的函数;或如果有许多函数访问某个字段。先使用 self encapsulate field

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

    决定如何在源对象中引用目标函数。

      首先,看是否有一个现成的字段、函数可以帮助得到目标对象。

      如果没有,就看能否轻易建立这样一个函数。

      如果不行,在源类中新建一个字段来存放目标对象,可能使一个永久性的修改,但也可以是暂时的。后续的重构可能会把这个新建字段除掉

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

      如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用

      如果需要对该变量赋值,就把对源字段的引用替换为对设值函数的调用

      如果源字段不是private,就必须在源类的所有子类中查找源字段的引用点,并进行相应替换 

      self encapsulate field使之可以小步前进。如果需要对类做许多处理,保持小步前进是有帮助的

        使用 self encapsulate field 使我得以更轻松使用move method将函数搬移到目标类中。如果待搬移函数引用了字段的访问函数,那些引用点是无需修改的

  

 3. extract class:提炼类

  某个类做了应该由两个类做的事

  建立一个新类,将相关的字段、函数从旧类搬移到新类

  

  动机:

    一个类应该是一个清楚的抽象,处理一些明确的责任

  • 在实际共迚,类会不断成长、扩展。加入一些功能、数据。

    给某个类添加一项责任时,会觉得不值得为这项责任分离出一个单独的类。随着责任不断增加,这个类会变得过分复杂。很快,类就会变成一团乱麻

    类中含有大量函数、数据。导致类太大,不易理解。

    需要考虑那些部分可以分离出去,并将其分离到一个单独的类中

    如果某些数据、函数总是一起出现,魔偶写数据经常同时变化,甚至彼此相依,就应该将其分离出去

    ?:如果搬移了某些字段、函数,会发生什么?其他字段、函数是否因此变得无意义?

  • 开发后期,类的子类化。

    如果发现子类化只影响类的部分特性;某些特性需要以一种方式来子类化,某些特征则需要以另一种方式子类话=》分解原来的类

  做法:

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

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

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

    对搬移的每个字段,运用move field,每次搬移后,编译、测试。

    move method将必要函数搬移到新类。先搬移较低层函数(“被其他函数调用”多余“调用其它函数”者),再搬移较高层函数。每次搬移后,编译、测试

    检查,精简每个类的接口。如果建立的是双向连接,检查是否可将其改为单向连接

    决定是否公开新类。如果需要公开,决定其为引用对象?不可变的值对象?

      如果选择公开新类,允许任何对象修改新类对象的任何部分。需要考虑公开带来的危险,尤其是set value。新类成为引用对象,应考虑使用change value to reference

      如果不允许任何人不通过源类就修改新类。可将新类设为不可修改的,或为其提供一个不可修改的接口;或者复制一个新类对象,将复制得到的新对象传递给用户。可能会造成一定程度的迷惑,让人误以为可以修改新类对象值(同一个对象被传递给多个用户,可能再用户之间造成别名问题)

    extract class,改善并发程序的一种常用技术,可使你为提炼后的两个类分别加锁,如果不需要同时锁定连个对象,就不必这样做。

      有一定的危险性。如果需要确保两个对象被同时锁定,就面临事务问题,需要使用其他类型的共享锁。这是个复杂领域,比一般情况需要更繁重的机制。

      事务很有实用性,但编写事务管理程序则超出了大多数程序员的职责范围

4. inline class 将类内联化

  某个类没有做太多事情

  将这个类的所有特性搬移到另一个类中,然后移除源类

  

  动机:

    与extract class相反。

    如果一个类不再承担足够责任,不再有单独存在的理由(通常,此前的重构动作移走了这个类的责任)

    挑选“萎缩类”的最频繁用户,以inline class 手法将“萎缩类”塞进另一个类中

  做法:

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

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

    编译,测试

    move field、move method将源类的特性全部搬移到目标类

5.hide delegate:隐藏委托关系

  客户通过一个委托类来调用另一个对象

  在服务类上建立客户所需的所有函数,用以隐藏委托关系。 

  

  动机:

    “封装”,即使不是对象的最关键特征,也是最关键特征之一。

      意味着:每一个对象都应该尽可能少了解系统的其他部分。

      一旦发生变化,需要了解这一变化的对象就会比较少,会使变化比较容易进行。

    字段虽然可以声明为public,但应该隐藏对象的字段

    如果某个客户先通过服务对象的字段得另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系

      万一委托关系发生变化,客户也得相应变化。

      可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖

      即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户

      

    对于某些、全部客户,可能有必要先使用extract class。一旦对所有客户都隐藏了委托关系,就不再需要在服务对象的接口中公开被委托的对象

  做法:

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

    调整客户,令其只调用服务对象提供的函数

      如果使用者、委托服务提供者不在同一包,考虑修改委托函数的访问权限,让客户得以在包之外调用它

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

6.remove middle man :移除中间人

  某个类做了过多的简单委托动作。

  让客户直接调用受托类

  

  动机:

    封装的代价:每当客户要使用受委托类的新特性时,必须再服务端添加一个简单委托函数。

      随着受托类的特性(功能)越来越多,这一过程让人痛苦不已

      服务类完全变成了一个“中间人”。此时应该让客户直接调用受托类

    很难说什么程度的隐藏才是合适的。

      hide delegate、remove middle man:可根据系统运行过程中不断进行调整。随着系统的变化,“合适的隐藏程度”这个尺度也在相应改变

      重构的意义在于:永远不必说对不起:只要把出问题的地方修补好就行了。

    也可能保留一部分委托关系。

      可能希望对某些客户隐藏委托关系,并让另一些用户直接使用受托对鞋。

      基于这些原因,一些简单的委托关系(及对应的委托函数),也可能被留在原地

  做法:

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

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

7.introduce foreign method:引入外加函数

  需要为提供服务的类增加一个函数,但无法修改这个类

  在客户类中建立一个函数,并以第一参数的形式传入一个服务类实例

  

  动机:

    正在使用一个类,为你提供了需要的所有服务。而后又需要一项新服务,这个类却无法供应。

    可自行添加一个新函数;如果无法添加,则在客户端编码,补足你要的那个函数

    如果客户类的功能只使用一次这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个类

    然而,如果需要多次使用这个函数,就得不断重复这些代码。重复代码是软件万恶之源。重复的代码应该被抽出来放进同一个函数中。

    在本项重构时,如果你以外加函数实现一项功能,那就是一个明确的信号:这个函数原本应该在提供服务的类中实现

    如果发现自己为一个服务类建立了大量的外加函数,或者发现许多类都需要同样的外加函数,就不应该再使用本项重构,而应该使用Introduce local extension

    外加函数终归时权宜之计。如果又可能,仍然应该将这些函数搬移到它们的理想家园。

      如果由于代码所有权的原因,使你无法做这样的搬移,就把外加函数交给服务类的拥有者,请他帮你再服务类中实现这个函数

  做法:

    在客户类中建立一个函数,用来提供你需要的功能。

      这个函数不应该调用客户类的任何特性。如果需要一个值,把该值当作参数传给它。便于将来迁移函数

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

    将函数注释为:“外加函数(foreign method),应在服务类实现”

      这样一来,如果将来有机会将外加函数搬移到服务类中,便可轻松找出这些外加函数

8. introduce local extension:引入本地扩展

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

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

  

  动机:

    类的作者无法预知未来,常常没能为你预先准备一些有用的函数。

      如果可以修改源码,最好的办法使直接加入自己需要的函数

      无法修改源码,如果只需要一两个函数,可以使用introduce foreign method。

      如果需要的额外函数超过两个,外加函数就很难控制它们了。需要将这些函数组织在一起,放到一个恰当的地方

    为了达到这个目的,两种标准对象技术:子类化subclassing、包装wrapping。统称为本地扩展。

      本地扩展:是一个独立的类,但也是被扩展类的子类型。提供源类的一切特性,同时额外添加新特性。

      在任何使用源类的地方都可以使用本地扩展取而代之

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

    在子类、包装类之间做选择,通常首选子类。工作量较少

      添加子类最大的障碍在于,必须在对象创建期实施。如果可以接管对象创建过程,当然没有问题;但如果想在对象创建之后再使用本地扩展就有问题了

      子类化方案,必须产生一个子类对象。这种情况下,如果有其他对象引用了旧对象,就同时有两个对象保存了原数据。如果原数据是不可修改的,则不会有问题,可以放心复制;如果原数据允许被修改,问题就会出现:一个修改动作无法同时改变两个副本

      使用包装类时,对本地扩展的修改会波及原对象,反之亦然。

    包装类有一个特殊问题:如何处理“接受原始类实例为参数”的函数。由于无法改变原始类,只能做到一个方向上的兼容:包装类上的函数可以接受包装类,或原始类;但原始类的函数只能接受原始类对象,不能接受包装类对象

      覆写的目的时为了向用户隐藏包装类的存在。这是一个好策略,因为包装类的用户的确不应该关心包装类的存在,的确应该可以同样的地对待包装类、原始类

      但是,完全无法隐藏包装类的存在,因为某些系统所提供的函数(例如equals())会出问题。

      可以在包装类中覆写equals(),但这样做时危险的,尽管达到了我们的目的,但在系统的其他部分都认为equals()符合交换律:包装类实例equals(被包装类实例),被包装类实例equals(包装类实例)。违反这一规则,容易使人遭受一大堆莫名其妙的错误

      要避免这样的尴尬境地,唯一的办法就是修改被包装类。但如果可以修改,何必进行此项重构

      在这种情况下,只能向用户公开“我进行了包装”这一事实。推荐使用一个新函数equal**()来实现相等性检查。亦可重载equal**(),一个接受被包装实例,一个接受包装实例。就不必检查未知对象的类型了

      子类化方案中就没有这样的问题,只要不覆写原函数就行。如果覆写了原始类中的函数,那么寻找函数时,就会被搞得晕头转向。一般,不建议在扩展类中覆写原始类的函数,只会添加新函数

      

  做法:

    建立一个扩展类,将它作为原始类的子类,或包装类

    在扩展类中加入转型构造函数

      “转型构造函数”:“接受原对象作为参数”的构造函数。

      如果采用子类化方案,转型构造函数应该调用适当的超类构造函数

      如果采用包装类方案,转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象

    在扩展类中加入新特性

    根据需要,将原对象替换为扩展对象

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

    

    

  

    

    

    

        

   

  

  

原文地址:https://www.cnblogs.com/panpanwelcome/p/7565076.html