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

如果你注定要成为厉害的人, 那问题的答案就深藏在你的血脉里。

本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第七章在对象之间搬移特性中 的知识点,

Move Method(搬移函数)

问题:你的程序中,有个函数与其所驻class之外的另一个class进行更多交流:调用后者,或被后者调用。

解决:在该函数最常引用的class中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数(delegating method),或是将旧函数完全移除。

动机

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

作法

  • 检查source class定义之source method所使用的一切特性(features),考虑它们是否也该被搬移。(译注:此处所谓特性泛指class定义的所有东西,包括值域和函数。)
  • 如果某个特性只被你打算搬移的那个函数用到,你应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候搬移一组函数比逐一搬移简单些。

范例

我用一个表示「帐户」的account class来说明这项重构:

class Account...
    //用户类型类
    private AccountType _type;
    //透支天数
    private int _daysOverdrawn;

  //透支费用
  double overdraftCharge() {                //译注:透支金计费,它和其他class的关系似乎比较密切。
      //判断保险
      if (_type.isPremium()) {
          double result = 10;
          if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
          return result;
      }
      else return _daysOverdrawn * 1.75;
  }
  //银行操作
  double bankCharge() {
      double result = 4.5;
      if (_daysOverdrawn > 0) result += overdraftCharge();
      return result;
  }
  

假设有数种新帐户,每一种都有自己的「透支金计费规则」。
所以我希望将overdraftCharge()搬移到AccountType class去。
第一步要做的是:观察被overdraftCharge()使用的每一特性(features),考虑是否值得将它们与overdraftCharge()—起移动。此例之中我需要让daysOverdrawn值域留在Account class,因为其值会随不同种类的帐户而变化。然后,我将overdraftCharge()函数码拷贝到AccountType中,并做相应调整。

class AccountType...
  double overdraftCharge(int daysOverdrawn) {
      if (isPremium()) {
          double result = 10;
          if (daysOverdrawn > 7) result += (daysOverdrawn - 7) * 0.85;
          return result;
      }
      else return daysOverdrawn * 1.75;
  }

在这个例子中,「调整」的意思是:(1)对于「使用AccountType特性」的语句,去掉_type;(2)想办法得到依旧需要的Account class特性。当我需要使用source class特性,我有四种选择:(1)将这个特性也移到target class;(2)建立或使用一个从target class到source的引用〔指涉)关系;(3)将source object当作参数传给target class;(4)如果所需特性是个变量,将它当作参数传给target method。
本例中我将_daysOverdrawn变量作为参数传给target method(上述(4))。
调整target method使之通过编译,而后我就可以将source method的函数本体替换为一个简单的委托动作(delegation),然后编译并测试:

class Account...
  double overdraftCharge() {
      return _type.overdraftCharge(_daysOverdrawn);
  }

我可以保留代码如今的样子,也可以删除source method。如果决定删除,就得找出source method的所有调用者,并将这些调用重新定向,改调用Account的bankCharge():

class Account...
  double bankCharge() {
      double result = 4.5;
      if (_daysOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn);
      return result;
  }

所有调用点都修改完毕后,我就可以删除source method在Account中的声明了。我可以在每次删除之后编译并测试,也可以一次性批量完成。如果被搬移的函数不是private,我还需要检查其他classes是否使用了这个函数。在强型(strongly typed) 语言中,删除source method声明式后,编译器会帮我发现任何遗漏。
此例之中被移函数只取用(指涉〕一个值域,所以我只需将这个值域作为参数传给target method就行了。如果被移函数调用了Account中的另一个函数,我就不能这么简单地处理。这种情况下我必须将source object传递给target method:

class AccountType...
  double overdraftCharge(Account account) {
      if (isPremium()) {
          double result = 10;
          if (account.getDaysOverdrawn() > 7)
             result += (account.getDaysOverdrawn() - 7) * 0.85;
          return result;
      }
      else return account.getDaysOverdrawn() * 1.75;
  }

如果我需要source class的多个特性,那么我也会将source object传递给target method。不过如果target method需要太多source class特性,就得进一步重构。通常这种情况下我会分解target method,并将其中一部分移回source class。

Move Field(搬移值域)

问题:你的程序中,某个field(值域〕被其所驻class之外的另一个class更多地用到。
解决:在target class 建立一个new field,修改source field的所有用户,令它们改用此new field。

动机

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

作法

  • 如果field的属性是public,首先使用Encapsulate Field(封装字段) 将它封装起来。
    Ø 如果你有可能移动那些频繁访问该field的函数,或如果有许多函数访问某个field,先使用Self Encapsulate Field 也许会有帮助。
  • 编译,测试。
  • 在target class中建立与source field相同的field,并同时建立相应的设值/取值 (setting/getting)函数。
  • 编译target class。
  • 决定如何在source object中引用target object。
    Ø 一个现成的field或method可以助你得到target object。如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在source class中新建一个field来存放target object。这可能是个永久性修改,但你也可以暂不公开它,因为后续重构可能会把这个新建field除掉。
  • 删除source field。
  • 将所有「对source field的引用」替换为「对target适当函数的调用」。
    Ø 如果是「读取」该变量,就把「对source field的引用」替换为「对target取值函数(getter)的调用」;如果是「赋值」该变量,就把对source field的引用」替换成「对设值函数(setter)的调用」。
    Ø 如果source field不是private,就必须在source class的所有subclasses中查找source field的引用点,并进行相应替换。

· 编译,测试。

范例

下面是Account class的部分代码:

class Account...
  private AccountType _type;
  private double _interestRate;
  double interestForAmount_days (double amount, int days) {
      return _interestRate * amount * days / 365;
  }

我想把表示利率的_interestRate搬移到AccountType class去。
目前已有数个函数引用了它,interestForAmount_days() 就是其一。
下一步我要在AccountType中建立_interestRate field以及相应的访问函数:

class AccountType...
  private double _interestRate;
  void setInterestRate (double arg) {
      _interestRate = arg;
  }
  double getInterestRate () {
      return _interestRate;
  }

这时候我可以编译新的AccountType class。
现在,我需要让Account class中访问此_interestRate field的函数转而使用AccountType对象,然后删除Account class中的_interestRate field。
我必须删除source field,才能保证其访问函数的确改变了操作对象,因为编译器会帮我指出未正确获得修改的函数。

  private double _interestRate;
  double interestForAmount_days (double amount, int days) {
      return _type.getInterestRate() * amount * days / 365;
  }

范例:使用Self Encapsulate(自我封装)

如果有很多函数已经使用了_interestRate field,我应该先运用Self Encapsulate Field:

class Account...
   private AccountType _type;
   private double _interestRate;
   double interestForAmount_days (double amount, int days) {
       return getInterestRate() * amount * days / 365;
   }
   private void setInterestRate (double arg) {
       _interestRate = arg;
   }
   private double getInterestRate () {
       return _interestRate;
   }

这样,在搬移field之后,我就只需要修改访问函数(accessors)就行了 :

   double interestForAmountAndDays (double amount, int days) {
       return getInterestRate() * amount * days / 365;
   }
   private void setInterestRate (double arg) {
      _type.setInterestRate(arg);
   }
   private double getInterestRate () {
       return _type.getInterestRate();
   }

如果以后有必要,我可以修改访问函数(accessors)的用户,让它们使用新对象。 Self Encapsulate Field 使我得以保持小步前进。如果我需要对做许多处理,保持小步前进是有帮助的。特别值得一提的是:首先使用Self Encapsulate Field 使我得以更轻松使用Move Method 将函数搬移到target class中。如果待移函数引用了field的访问函数(accessors),那么那些引用点是无须修 改的。

Extract Class(提炼类)

问题:某个class做了应该由两个classes做的事。

解决:建立一个新class,将相关的值域和函数从旧class搬移到新class。

动机

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

作法

  • 决定如何分解c;ass所负责任。
  • 建立一个新class,用以表现从旧class中分离出来的责任。
    Ø 如果旧class剩下的责任与旧class名称不符,为旧class易名。
  • 建立「从旧class访问新class」的连接关系(link)。
    Ø 也许你有可能需要一个双向连接。但是在真正需要它之前,不要建立 「从新class通往旧class」的连接。
  • 对于你想搬移的每一个值域,运用Move Field 搬移之。
  • 每次搬移后,编译、测试。
  • 使用Move Method 将必要函数搬移到新class。先搬移较低层函数(也就是「被其他函数调用」多于「调用其他函数」者),再搬移较高层函数。
  • 每次搬移之后,编译、测试。
  • 检查,精简每个class的接口。
    Ø 如果你建立起双向连接,检查是否可以将它改为单向连接。
  • 决定是否让新class曝光。如果你的确需要曝光它,决定让它成为reference object (引用型对象〕或immutable value object(不可变之「实值型对象」)。

范例

让我们从一个简单的Person class开始

class Person...

   private String _name;
   private String _officeAreaCode;
   private String _officeNumber;


   public String getName() {
       return _name;
   }
   public String getTelephoneNumber() {
       return ("(" + _officeAreaCode + ") " + _officeNumber);
   }
   String getOfficeAreaCode() {
       return _officeAreaCode;
   }
   void setOfficeAreaCode(String arg) {
       _officeAreaCode = arg;
   }
   String getOfficeNumber() {
       return _officeNumber;
   }
   void setOfficeNumber(String arg) {
       _officeNumber = arg;
   }
   

在这个例子中,我可以将「与电话号码相关」的行为分离到一个独立class中。首 先我耍定义一个TelephoneNumber class来表示「电话号码」这个概念:

class TelephoneNumber {
}

易如反掌!然后,我要建立从Person到TelephoneNumber的连接:

class Person
   private TelephoneNumber _officeTelephone = new TelephoneNumber();

现在,我运用Move Field 移动一个值域:

class Person...
   public String getName() {
       return _name;
   }
   public String getTelephoneNumber(){
       return _officeTelephone.getTelephoneNumber();
   }
   TelephoneNumber getOfficeTelephone() {
       return _officeTelephone;
   }
   private String _name;
   private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
   public String getTelephoneNumber() {
       return ("(" + _areaCode + ") " + _number);
   }
   String getAreaCode() {
       return _areaCode;
   }
   void setAreaCode(String arg) {
       _areaCode = arg;
   }
   String getNumber() {
       return _number;
   }
   void setNumber(String arg) {
       _number = arg;
   }
   private String _number;
   private String _areaCode;

下一步要做的决定是:要不要对客户揭示这个新口class?我可以将Person中「与电 话号码相关」的函数委托(delegating)至TelephoneNumber,从而完全隐藏这个新class;也可以直接将它对用户曝光。我还可以将它暴露给部分用户(位于同一个package中的用户),而不暴露给其他用户。
如果我选择暴露新class,我就需要考虑别名(aliasing)带来的危险。如果我暴露了TelephoneNumber ,而有个用户修改了对象中的_areaCode值域值,我又怎么能知道呢?而且,做出修改的可能不是直接用户,而是用户的用户的用户。
面对这个问题,我有下列数种选择:

  1. 允许任何对象修改TelephoneNumber 对象的任何部分。这就使得TelephoneNumber 对象成为引用对象(reference object),于是我应该考虑使用 Change Value to Reference。这种情况下,Person应该是TelephoneNumber的访问点。
  2. 不许任何人「不通过Person对象就修改TelephoneNumber 对象」。为了达到目的,我可以将TelephoneNumber「设为不可修改的(immutable),或为它提供一个不可修改的接口(immutable interface)。
  3. 另一个办法是:先复制一个TelephoneNumber 对象,然后将复制得到的新对象传递给用户。但这可能会造成一定程度的迷惑,因为人们会认为他们可以修改TelephoneNumber对象值。此外,如果同一个TelephoneNumber 对象 被传递给多个用户,也可能在用户之间造成别名(aliasing)问题。

Extract Class 是改善并发(concurrent)程序的一种常用技术,因为它使你可以为提炼后的两个classes分别加锁(locks)。如果你不需要同时锁定两个对象, 你就不必这样做。这方面的更多信息请看Lea[Lea], 3.3节。
这里也存在危险性。如果需要确保两个对象被同时锁定,你就面临事务(transaction)问题,需要使用其他类型的共享锁〔shared locks〕。正如Lea[Lea] 8.1节所讨论, 这是一个复杂领域,比起一般情况需要更繁重的机制。事务(transaction)很有实用性,但是编写事务管理程序(transaction manager)则超出了大多数程序员的职责范围。

Inline Class(将类内联化)

问题:你的某个class没有做太多事情(没有承担足够责任)。

解决:将class的所有特性搬移到另一个class中,然后移除原class。

动机

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

作法

  • 在absorbing class(合并端的那个class)身上声明source class的public协议, 并将其中所有函数委托(delegate)至source class。
    Ø 如果「以一个独立接口表示source class函数」更合适的话,就应该在inlining之前先使用Extract Interface。
  • 修改所有source class引用点,改而引用absorbing class。
    Ø 将source class声明为private,以斩断package之外的所有引用可能。 同时并修改source class的名称,这便可使编译器帮助你捕捉到所有对于source class的"dangling references "(虚悬引用点)。
  • 编译,测试。
  • 运用Move Method 和 Move Field ,将source class的特性全部搬移至absorbing class。
  • 为source class举行一个简单的丧礼。

范例

先前(上个重构项〉我从TelephoneNumber「提炼出另一个class,现在我要将它inlining塞回到Person去。一开始这两个classes是分离的:

class Person...


   private String _number;
   private String _areaCode;

   public String getName() {
       return _name;
   }
   public String getTelephoneNumber(){
       return _officeTelephone.getTelephoneNumber();
   }
   TelephoneNumber getOfficeTelephone() {
       return _officeTelephone;
   }
   private String _name;
   private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
   public String getTelephoneNumber() {
       return ("(" + _areaCode + ") " + _number);
   }
   String getAreaCode() {
       return _areaCode;
   }
   void setAreaCode(String arg) {
       _areaCode = arg;
   }
   String getNumber() {
       return _number;
   }
   void setNumber(String arg) {
       _number = arg;
   }

首先我在Person中声明TelephoneNumber「的所有「可见」(public)函数:

class Person...
   String getAreaCode() {
       return _officeTelephone.getAreaCode();        //译注:请注意其变化
   }
   void setAreaCode(String arg) {
       _officeTelephone.setAreaCode(arg);                //译注:请注意其变化
   }
   String getNumber() {
       return _officeTelephone.getNumber();        //译注:请注意其变化
   }
   void setNumber(String arg) {
       _officeTelephone.setNumber(arg);                //译注:请注意其变化
   }

现在,我要找出TelephoneNumber的所有用户,让它们转而使用Person接口。于是下列代码:

  Person martin = new Person();
  martin.getOfficeTelephone().setAreaCode ("781");

就变成了:

       Person martin = new Person();
       martin.setAreaCode ("781");

现在,我可以持续使用Move Method 和 Move Field ,直到TelephoneNumber不复存在。

Hide Delegate(隐藏「委托关系」)

问题:客户直接调用其server object(服务对象)的delegate class。

解决:在server端(某个class〕建立客户所需的所有函数,用以隐藏委托关系(delegation)。

动机

封装」即使不是对象的最关键特征,也是最关键特征之一。「封装」意味每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一 变化的对象就会比较少——这会使变化比较容易进行。
任何学过对象技术的人都知道:虽然Java允许你将值域声明为public,但你还是应该隐藏对象的值域。随着经验日渐丰富,你会发现,有更多可以(并值得)封装的东西。
如果某个客户调用了「建立于server object (服务对象)的某个值域基础之上」的函数,那么客户就必须知晓这一委托对象(delegate object。译注:即server object的那个特殊值域)。万一委托关系发生变化,客户也得相应变化。你可以在server 端放置一个简单的委托函数(delegating method),将委托关系隐藏起来,从而去除这种依存性(图7.1)。这么一来即便将来发生委托关系上的变化,变化将被限制在server中,不会波及客户。

对于某些客户或全部客户,你可能会发现,有必要先使用Extract Class 。一旦你对所有客户都隐藏委托关系(delegation),你就可以将server 接口中的所有 委托都移除。

作法

  • 对于每一个委托关系中的函数,在server端建立一个简单的委托函数(delegating method)。
  • 调整客户,令它只调用server 提供的函数(译注:不得跳过径自调用下层)。
    Ø 如果client (客户〕和server不在同一个package,考虑修改委托函数 (delegate method)的访问权限,让client得以在package之外调用它。
  • 每次调整后,编译并测试。
  • 如果将来不再有任何客户需要取用图7.1的Delegate (受托类),便可移除server中的相关访问函数(accessor for the delegate)。
  • 编译,测试。

范例

本例从两个classes开始,代表「人」的Person和代表「部门」的Department:

class Person {
   Department _department;
   public Department getDepartment() {
       return _department;
   }
   public void setDepartment(Department arg) {
       _department = arg;
   }
}
class Department {
   private String _chargeCode;
   private Person _manager;
   public Department (Person manager) {
       _manager = manager;
   }
   public Person getManager() {
       return _manager;
   }
...

如果客户希望知道某人的经理是谁,他必须先取得Department对象:

manager = john.getDepartment().getManager();


这样的编码就是对客户揭露了Department的工作原理,于是客户知道:Department用以追踪「经理」这条信息。
如果对客户隐藏Department,可以减少耦合(coupling)。 为了这一目的,我在Person中建立一个简单的委托函数:

   public Person getManager() {
       return _department.getManager();
   }

现在,我得修改Person的所有客户,让它们改用新函数:

manager = john.getManager();

只要完成了对Department所有函数的委托关系,并相应修改了Person的所有客 户,我就可以移除Person中的访问函数getDepartment()了。

Remove Middle Man(移除中间人)

问题:某个class做了过多的简单委托动作(simple delegation)。

解决:让客户直接调用delegate(受托类)。

动机

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

做法

  • 建立一个函数,用以取用delegate(受托对象)。
  • 对于每个委托函数(delegate method),在server中删除该函数,并将「客户对该函数的调用」替换为「对delegate(受托对象)的调用」。
  • 处理每个委托函数后,编译、测试。

范例

我将以另一种方式使用先前用过的「人与部门」例子。还记得吗,上一项重构结束时,Person将Department隐藏起来了:

class Person...
   Department _department;       
   public Person getManager() {
       return _department.getManager();
class Department...
   private Person _manager;
   public Department (Person manager) {
       _manager = manager;
   }

为了找出某人的经理,客户代码可能这样写:

manager = john.getManager();

像这样,使用和封装Department都很简单。但如果大量函数都这么做,我就不得不在Person之中安置大量委托行为(delegations)。这就是移除中间人的时候了。 首先在Person建立一个「受托对象(delegate)取得函数」:

class Person...
   public Department getDepartment() {
       return _department;
   }

然后逐一处理每个委托函数。针对每一个这样的函数,我要找出通过Person使用的函数,并对它进行修改,使它首先获得受托对象(delegate),然后直接使用之:
manager = john.getDepartment().getManager();
然后我就可以删除Person的getManager() 函数。如果我遗漏了什么,编译器会 告诉我。
为方便起见,我也可能想要保留一部分委托关系(delegations)。此外我也可能希望对某些客户隐藏委托关系,并让另一些用户直接使用受托对象。基于这些原因,一些简单的委托关系(以及对应的委托函数)也可能被留在原地。

Introduce Foreign Method(引入外加函数)

问题:你所使用的server class需要一个额外函数,但你无法修改这个class。

解决:在client class中建立一个函数,并以一个server class实体作为第一引数(argument)

 Date newStart = new Date (previousEnd.getYear(),
                    previousEnd.getMonth(), previousEnd.getDate() + 1);
 
    Date newStart = nextDay(previousEnd);
    private static Date nextDay(Date arg) {
        return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
    }

动机

这种事情发生过太多次了:你正在使用一个class,它真的很好,为你提供了你想要的所有服务。
而后,你又需要一项新服务,这个class却无法供应。
于是你开始咒骂:「为什么不能做这件事?」如果可以修改源码,你便可以自行添加一个新函数; 如果不能,你就得在客户端编码,补足你要的那个函数。
如果client class只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个class。然而如果你需要多次使用这个函数,你就得不断重复这些代码。还记得吗,重复的代码是软件万恶之源。这些重复性代码应该被抽出来放进同一个函数中。进行本项重构时,如果你以外加函数实现一项功能, 那就是一个明确信号:这个函数原本应该在提供服务的(server)class中加以实现。
如果你发现自己为一个server class建立了大量外加函数,或如果你发现有许多classes都需要同样的外加函数,你就不应该再使用本项重构,而应该使用 Introduce Local Extension。
但是不要忘记:外加函数终归是权宜之计。如果有可能,你仍然应该将这些函数搬移到它们的理想家园。如果代码拥有权(code ownership)是个需要考量的问题, 就把外加函数交给server class的拥有者,请他帮你在此server class中实现这个函数。

作法

  • 在client class中建立一个函数,用来提供你需要的功能。
    Ø 这个函数不应该取用client class的任何特性。如果它需要一个值,把该值当作参数传给它。
  • 以server class实体作为该函数的第一个参数。
  • 将该函数注释为:「外加函数(foreign method),应在server class实现。」
    Ø 这么一来,将来如果有机会将外加函数搬移到server class中,你便可以轻松找出这些外加函数。

范例

程序中,我需要跨过一个收费周期(billing period)。原本代码像这样:

    Date newStart = new Date (previousEnd.getYear(),
         previousEnd.getMonth(), previousEnd.getDate() + 1);

我可以将赋值运算右侧代码提炼到一个独立函数中。这个函数就是Date class的一个外加函数:

Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg) {
// foreign method, should be on date
     return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
}

Introduce Local Extension(引入本地扩展)

问题:你所使用的server class需要一些额外函数,但你无法修改这个class。
解决:建立一个新class,使它包含这些额外函数。让这个扩展品成为source class的subclass (子类〕或wrapper(外覆类)。

动机

很遗憾,classes的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。
如果你可以修改源码,最好的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用Introduce Foreign Method。

但如果你需要的额外函数超过两个,外加函数(foreign methods)就很难控制住它 们了。所以,你需要将这些函数组织在一起,放到一个恰当地方去。要达到这一目 的,标准对象技术subclassing和wrapping是显而易见的办法。这种情况下我把 subclass 或wrapper称为local extention(本地扩展〕。

所谓本地扩展是一个独立的class,但也是被扩展的子类型。这意味它提供original class的一切特性,同时并额外添加新特性。在任何使用original class的地方,你都可以使用local extention取而代之。

作法

  • 建立一个extension class,将它作为原物(原类〉的subclass或wrapper。
  • 在extension class 中加入转型构造函数(converting constructors )。
    Ø 所谓「转型构造函数」是指接受原物(original)作为参数。如果你釆用subclassing方案,那么转型构造函数应该调用适当的subclass构造函数;如果你采用wrapper方案,那么转型构造函数应该将它所获得之引数(argument)赋值给「用以保存委托关系(delegate)」的那个值域。
  • 在extension class中加入新特性。
  • 根据需要,将原物(original)替换为扩展物(extension)。
  • 将「针对原始类(original class)而定义的所有外加函数(foreign methods)」 搬移到扩展类extension中。

范例

我将以Java 1.0.1的Date class为例。Java 1.1已经提供了我想要的功能,但是在它到来之前的那段日子,很多时候我需要扩展Java 1.0.1的Date class。
第一件待决事项就是使用subclass或wrapper。subclassing是比较显而易见的办法:

Class mfDate extends Date {
   public nextDay()...
   public dayOfYear()...

wrapper则需要用上委托(delegation):

                                  
class mfDate {
   private Date _original;

范例:是用Subclass(子类)

首先,我要建立一个新的MfDateSub class来表示「日期」(译注:"Mf"是作者Martin Fowler的姓名缩写),并使其成为Date的subclass:

  class MfDateSub extends Date

然后,我需要处理Date 和我的extension class之间的不同处。MfDateSub 构造函数需要委托(delegating)给Date构造函数:

public MfDateSub (String dateString) {
      super (dateString);
};

现在,我需要加入一个转型构造函数,其参数是一个隶属原类的对象:

  public MfDateSub (Date arg) {
      super (arg.getTime());
  }

现在,我可以在extension class中添加新特性,并使用Move Method 将所有外加函数(foreign methods)搬移到extension class。于是,下面的代码:

client class...
    private static Date nextDay(Date arg) {
    // foreign method, should be on date
        return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
    }

经过搬移之后,就成了:

  class MfDate...
    Date nextDay() {
        return new Date (getYear(),getMonth(), getDate() + 1);
  }

范例:是用wrapper(外覆类)

首先声明一个wrapping class:

  class mfDate {
    private Date _original;
  }

使用wrapping方案时,我对构造函数的设定与先前有所不同。现在的构造函数将只是执行一个单纯的委托动作(delegation):

   public MfDateWrap (String dateString) {
       _original = new Date(dateString);
   };

而转型构造函数则只是对其instance变量赋值而己:

   public MfDateWrap (Date arg) {
       _original = arg;
   }

接下来是一项枯燥乏味的工作:为原始类的所有函数提供委托函数。我只展示两个函数,其他函数的处理依此类推。

   public int getYear() {
       return _original.getYear();
   }
   public boolean equals (MfDateWrap arg) {
       return (toDate().equals(arg.toDate()));
   }

完成这项工作之后,我就可以后使用Move Method 将日期相关行为搬移到新class中。于是以下代码:

  client class...
    private static Date nextDay(Date arg) {
    // foreign method, should be on date
        return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1);
    }

经过搬移之后,就变成:

  class MfDate...
    Date nextDay() {
        return new Date (getYear(),getMonth(), getDate() + 1);
  }

使用wrappers有一个特殊问题:如何处理「接受原始类之实体为参数」的函数?例如:

  public boolean after (Date arg)

由于无法改变原始类〔original),所以我只能以一种方式使用上述的after() :

  aWrapper.after(aDate)                         // can be made to work
  aWrapper.after(anotherWrapper)                // can be made to work
  aDate.after(aWrapper)                         // will not work

这样覆写(overridden)的目的是为了向用户隐藏wrapper 的存在。这是一个好策略,因为wrapper 的用户的确不应该关心wrapper 的存在,的确应该可以同样地对待wrapper(外覆类)和orignal((原始类)。但是我无法完全隐藏此一信息,因为某些系统所提供的函数(例如equals() 会出问题。
你可能会认为:你可以在MfDateWrap class 中覆写equals(),像这样:

  public boolean equals (Date arg)     // causes problems

但这样做是危险的,因为尽管我达到了自己的目的,Java 系统的其他部分都认为equals() 符合交换律:如果a.equals(b)为真,那么b.equals(a)也必为真。违反这一规则将使我遭遇一大堆莫名其妙的错误。
要避免这样的尴尬境地,惟一办法就是修改Date class。但如果我能够修改Date ,我又何必进行此项重构?所以,在这种情况下,我只能(必须〕向用户暴露「我进行了包装」这一事实。我将以一个新函数来进行日期之间的相等性检查(equality tests):

 public boolean equalsDate (Date arg)

我可以重载equalsDate() ,让一个重载版本接受Date 对象,另一个重载版本接受MfDateWrap 对象。这样我就不必检查未知对象的型别了:

  public boolean equalsDate (MfDateWrap arg)

subclassing方案中就没有这样的问题,只要我不覆写原函数就行了。
但如果我覆写了original class 中的函数,那么寻找函数时,我会被搞得晕头转向。一般来说,我不会在extension class 中覆写0original class 的函数,我只会添加新函数。

译注:equality(相等性)是一个很基础的大题目。《Effective Java》 by Joshua Bloch 第3章,以及《Practical Java》by Peter Haggar 第2章,对此均有很深入的讨论。这两本书对于其他的基础大题目如Serizable,Comparable,Cloneable,hashCode() 也都有深刻讨论。

感谢观看 你肯定有收获对吧

你可能感兴趣的:(重构和设计模式,重构,java)