重构-在对象之间搬移特性

《重构:改善既有代码的设计》一书学习笔记。

原则

  1. 不要让一个类承担太多责任
  2. 迪米特法则:一个对象应该对其他对象保持最少的了解,类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
  3. 能复用的代码尽量抽取,引入外加函数或引入本地扩展

方法

Move Method(搬移函数)

A类中有个函数a,并且含有一个类引用B,如果a函数的逻辑与B相关比较大,则可以考虑将a函数搬移到B中。

Move Field(搬移字段)

对于一个字段,在其所驻类之外的另一个类中有更多的函数使用它,则考虑搬移该字段。或者搬移另一个类的函数,这取决于实际情况。

Extract Class(提炼类)

一个类应该是一个清楚的抽象,处理一些明确的责任。但是在实际开发中,随着业务越来越大,类会不断扩展。当给某个类加一个新的责任时,你会觉得这点责任不足以为它新增一个类,于是随着责任不断增加,这个类就会变得过分复杂。

做法

  • 决定如果分解类的责任
  • 建立一个新类,用以承载旧类的某些责任。
  • 建立从旧类访问新类的连接关系,有可能是双向连接,但是尽量不要建立新类到旧类的连接。

范例

class Person...
   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;
   }
   private String _name;
   private String _officeAreaCode;
   private String _officeNumber;

可以将电话号码相关的内容抽取出来,变为TelephoneNumber类 如下:

class Person...
    private String _name;
   private TelephoneNumber _officeTelephone = new TelephoneNumber();
   public String getName() {
       return _name;
   }
   public String getTelephoneNumber(){
       return _officeTelephone.getTelephoneNumber();
   }
   TelephoneNumber getOfficeTelephone() {
       return _officeTelephone;
   }

class TelephoneNumber...
   private String _number;
   private String _areaCode;
   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;
   }

这样做后TelephoneNumber便成了Person的一个属性,但是还需要思考一个问题,要不要把TelephoneNumber暴露出去,如果暴露出去,那么其他类直接改TelephoneNumber的内容,Pserson是不知情的,这就会带来一些问题。所以非必要情况下,可以只暴露某些方法,或者只暴露给部分用户,也就是protect或者default作用域下的

Inline Class(将类内联化)

你的某个class没有做太多事情(没有承担足够责任)。
将class的所有特性搬移到另一个class中,然后移除原class。
与提炼类正好相反,如果一个class不再承担足够 责任、不再有单独存在的理由〔这通常是因为此前的重构动作移走了这个class的 责任),这时候找出与这个class关联最频繁的class,将两个class合并成一个。与上述相同,不做赘述了。

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

「封装」意味每个对象都应该尽可能少了解系统的其他部分。如此一来,一旦发生变化,需要了解这一 变化的对象就会比较少——这会使变化比较容易进行。
这个原则有点像迪米特法则,即一个对象应该对其他对象保持最少的了解。
我们从一个例子来理解

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(受托类)。
这个与上面的隐藏委托关系正好相反。这个很好理解,如果一个类委托了另一个类的所有方法,那么就没必要设置委托了。

Introduce Foreign Method(引入外加函数)

你正在使用一个class,它真的很好,为你提供了你想要的所有服务。而后,你又需要一项新服务,这个class却无法供应。于是你开始咒骂:「为什么不能做这件事?」如果可以修改源码,你便可以自行添加一个新函数; 如果不能,你就得在客户端编码,补足你要的那个函数。
如果client class只使用这项功能一次,那么额外编码工作没什么大不了,甚至可能根本不需要原本提供服务的那个class。然而如果你需要多次使用这个函数,你就得不断重复这些代码,这些重复性代码应该被抽出来放进同一个函数中。如果你发现自己为一个server class建立了大量外加函数,或如果你发现有许多classes都需要同样的外加函数,你就不应该再使用本项重构,而应该使用 Introduce Local Extension。

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);
} 

Introduce Local Extension(引入本地扩展)

同样的,当一个类中,没有你需要的函数时,但是这个函数需要被大量的classes使用,这时候就需要使用本方法。如果这个类可以修改其源码,那么可以直接在源码中修改,但是大部分情况应该是不可以修改的。这时候就可以建立一个新class,使它包含这些额外函数。可以成为source class的subclass (子类〕或wrapper(外覆类)

使用subclass

首先,我要建立一个新的DateSub class来表示「日期」,并使其成为Date的subclass:

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

当需要获得nextDay时,就可调用DateSub中的nextDay();

使用wrapper

声明一个wrapping class:

class DateWrapper {
   private Date _original;
   public DateWrapper (String dateString) {
      _original = new Date(dateString);
      };
 }