资讯专栏INFORMATION COLUMN

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

BlackHole1 / 1637人阅读

摘要:本篇文章主要讲解重构改善既有代码的设计这本书中的第七章在对象之间搬移特性中的知识点,搬移函数问题你的程序中,有个函数与其所驻之外的另一个进行更多交流调用后者,或被后者调用。动机在之间移动状态和行为,是重构过程中必不可少的措施。

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

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

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值域值,我又怎么能知道呢?而且,做出修改的可能不是直接用户,而是用户的用户的用户。
面对这个问题,我有下列数种选择:

允许任何对象修改TelephoneNumber 对象的任何部分。这就使得TelephoneNumber 对象成为引用对象(reference object),于是我应该考虑使用 Change Value to Reference。这种情况下,Person应该是TelephoneNumber的访问点。

不许任何人「不通过Person对象就修改TelephoneNumber 对象」。为了达到目的,我可以将TelephoneNumber「设为不可修改的(immutable),或为它提供一个不可修改的接口(immutable interface)。

另一个办法是:先复制一个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() 也都有深刻讨论。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70755.html

相关文章

  • 重构-改善既有代码设计(七)-- 在代码之间搬移特性

    摘要:前言决定把责任放在哪对于对象设计是最重要的之一。重构可以很好的解决这个问题。方法建立一个新类,将相关的字段和函数从旧类搬移到新类。方法将这个类的所有特性搬移到另一个类中,然后移除原类。让这个扩展品成为源类的子类或包装类。 前言 决定把责任放在哪对于对象设计是最重要的之一。重构可以很好的解决这个问题。以下是笔者的重构方法注:客户:调用接口客户类:使用了接口的类服务类:提供服务的类 Mov...

    solocoder 评论0 收藏0
  • 读书笔记《重构 改善既有代码设计

    摘要:重构在不改变代码的外在的行为的前提下对代码进行修改最大限度的减少错误的几率本质上,就是代码写好之后修改它的设计。重构可以深入理解代码并且帮助找到。同时重构可以减少引入的机率,方便日后扩展。平行继承目的在于消除类之间的重复代码。 重构 (refactoring) 在不改变代码的外在的行为的前提下 对代码进行修改最大限度的减少错误的几率 本质上, 就是代码写好之后 修改它的设计。 1,书中...

    mdluo 评论0 收藏0
  • 重构---改善既有代码设计

    摘要:为何重构重构有四大好处重构改进软件设计如果没有重构,程序的设计会逐渐腐败变质。经常性的重构可以帮助维持自己该有的形态。你有一个大型函数,其中对局部变量的使用使你无法采用。将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。 哪有什么天生如此,只是我们天天坚持。 -Zhiyuan 国庆抽出时间来阅读这本从师傅那里借来的书,听说还是程序员的必读书籍。 关于书的高清下载连...

    baihe 评论0 收藏0
  • 重构-改善既有代码设计(一)--重构,第一个案例

    摘要:并根据目录选读第章重构,第一个案例这是只是一个方法。绝大多数情况下,函数应该放在它所使用的数据的所属对象内最好不要在另一个对象的属性基础上运用语句。 什么是重构 在不改变代码外在行为的前提下,对代码做出修改以改进程序内部的结构简单地说就是在代码写好后改进它的设计 谁该阅读这本书 专业程序员(能够提高你的代码质量) 资深设计师和架构规划师(理解为什么需要重构,哪里需要重构) 阅读技巧...

    acrazing 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<