《重构---改善既有代码的设计》之简化函数调用

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

本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第十章简化函数调用中 的知识点,

Rename Method(重新命名函数)

问题:函数的名称未能揭示函数的用途

解决:修改函数名称。

clipboard.png

动机

将复杂的处理过程分解成小函数。
但是,如果做得不好,这会使你费尽周折却弄不清楚这些小函数各自的用途。要避免这种麻烦,关键就在于给函数起一个好名称。函数的名称应该准确表达它的用途。
给函数命名有一个好办法:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称
函数签名式(signature)中的其他部分也一样重要;如果重新安排参数顺序,能够帮助提高代码的清晰度,那就大胆地去做吧,你有 Add Parameter 和Remove Parameter 这两项武器。

如何给函数命名

做法

  • 检查函数签名式(signature)是否被superclass 或subclass 实现过。如果是,则需要针对每份实现品分别进行下列步骤。
  • 声明一个新函数,将它命名为你想要的新名称。将旧函数的代码拷贝到新函数中,并进行适当调整。
  • 如果旧函数是class public 接口的一部分,你可能无法安全地删除它。这种情况下,将它保留在原处,并将它标记为"deprecated"(不建议使用)。

Add Parameter(添加参数)

问题:某个函数需要从调用端得到更多信息
做法:为此函数添加一个对象参数,让该对象带进函数所需信息。

clipboard.png

动机

使用这项重构的动机很简单:你必须修改一个函数,而修改后的函数需要一些过去没有的信息,因此你需要给该函数添加一个参数。

实际上我比较需要说明的是:不使用本重构的时机。除了添加参数外,你常常还有其他选择。只要可能,其他选择都比本项「添加参数」要好,因为它们不会增加参数列的长度。过长的参数列是不好的味道,因为程序员很难记住那么多参数,而且长参数列往往伴随着坏味道Date Clumps。
我并非要你绝对不要添加参数。事实上我自己经常添加参数,但是在添加参数之前你有必要了解其他选择。

做法

Add Parameter 的作法和Rename Method 非常相似。

  • 检查函数签名式(signature)是否被superclass 或subclass 实现过。如果是,则需要针对每份实现分别进行下列步骤。
  • 声明一个新函数,名称与原函数同,只是加上新添参数。将旧函数的代码拷贝到新函数中。
  • 此时,你可以给参数提供任意值。但一般来说,我们会给对象参数提供null ,给内置型参数提供一个明显非正常值。对于数值型参数,我建议使用0 以外的值,这样你比较容易将来认出它。
  • 编译,测试。

Remove Parameter(移除参数)

问题:函数本体(method body)不再需要某个参数。
解决:将该参数去除。

clipboard.png

动机

程序员可能经常添加参数,却往往不愿意去掉它们。他们打的如意算盘是,无论如 何,多余的参数不会引起任何问题,而且以后还可能用上它。

参数指出函数所需信息,不同的参数值代表不同的意义。函数调用者必须为每一个参数操心该传什么东西进去。如果你不去掉多余参数,你就是让你的每一位用户多费一份心。这是很不划算的,尤其「去除参数」是非常简单的一项重构。

但是,对于多态函数(polymorphic method),情况有所不同。这种情况下,可能多态函数的另一份(或多份)实现码会使用这个参数,此时你就不能去除它。

做法

Remove Parameter 的作法和Rename Method 、Add Parameter 非常相似。

  • 检查函数签名式(signature)是否被superclass 或如subclass 实现过。如果是,则需要针对每份实现品分别进行下列步骤。
  • 其他的和上边两个一样

由于我可以轻松地添加、去除参数,所以我经常一次性地添加或去除必要的参数。

Separate Query from Modifier(将查询函数和修改函数分离)

问题:某个函数既返回对象状态值,又修改对象状态(state)

解决:建立两个不同的函数,其中一个负责査询,另一个负责修改

clipboard.png

动机

如果某个函数只是向你提供一个值,没有任何看得到的副作用(或说连带影响),那么这是个很有价值的东西。
你可以任意调用这个函数,也可以把调用动作搬到函 数的其他地方。

下而是一条好规则:任何有返回值的函数,都不应该有看得到的副作用。
有些程序 员甚至将此作为一条必须遵守的规则[Meyer]。
就像对待任何东西一样,我并不绝对遵守它,不过我总是尽量遵守,而它也回报我很好的效果

有一种常见的优化办法是:将查询所得结果高速缓存(cache)于某个值域中,这么一来后续的重复查询 就可以大大加快速度。虽然这种作法改变了对象的状态,但这一修改是察觉不到的,因为不论如何査询,你总是获得相同结果[Meyer]。

做法

  • 新建一个查询函数,令它返回的值与原函数相同。
  • 观察原函数,看它返回什么东西。如果返回的是一个临时变量,找出临时变量的位置。
  • 修改原函数,令它调用查询函数,并返回获得的结果。
  • 如果调用者将返回值赋给了一个临时变量,你应该能够去除这个临时 变量。
  • 编译,测试。
  • 将「原函数的每一个被调用点」替换为「对查询函数的调用」。然后,在调用査询函数的那一行之前,加上对原函数的调用。每次修改后,编译并测试。
  • 将原函数的返回值改为void。并删掉其中所有的return 句。

范例

有这样一个函数:一旦有人入侵安全系统,它会告诉我入侵者的名字,并发送一个警报。如果入侵者不止一个,也只发送一条警报:

//寻找歹徒
String foundMiscreant(String[] people){
      for (int i = 0; i < people.length; i++) {
          if (people[i].equals ("Don")){
             sendAlert();
             return "Don";
          }
          if (people[i].equals ("John")){
             sendAlert();
             return "John";
          }
      }
      return "";
  }

该函数被下列代码调用:

//检查安全
  void checkSecurity(String[] people) {
      String found = foundMiscreant(people);
      //do something
      someLaterCode(found);
  }

为了将查询动作和修改动作分开,我首先建立一个适当的查询函数,使其与修改函数返回相同的值,但不造成任何副作用:

//找人
   String foundPeople(String[] people){
       for (int i = 0; i < people.length; i++) {
           if (people[i].equals ("Don")){
              return "Don";
           }
           if (people[i].equals ("John")){
              return "John";
           }
       }
       return "";
   }

然后,我要逐一替换原函数内所有的return语句,改调用新建的查询函数。每次替换后,编译并测试。这一步完成之后,原函数如下所示:

  String foundMiscreant(String[] people){
      for (int i = 0; i < people.length; i++) {
          if (people[i].equals ("Don")){
             sendAlert();
             return foundPerson(people);
          }
          if (people[i].equals ("John")){
             sendAlert();
             return foundPerson(people);
          }
      }
      return foundPerson(people);
  }

现在,我要修改调用者,将原本的单一调用动作替换为两个调用:先调用修改函数,然后调用查询函数:

  void checkSecurity(String[] people) {
      //寻找歹徒
      foundMiscreant(people);
      //找人
      String found = foundPerson(people);
      //do something
      someLaterCode(found);
  }

现在,为原函数改个名称可能会更好一些:

  void sendAlert (String[] people){
      for (int i = 0; i < people.length; i++) {
          if (people[i].equals ("Don")){
             sendAlert();
             return;
          }
          if (people[i].equals ("John")){
             sendAlert();
             return;
          }
      }
  }

当然,这种情况下,我得到了大量重复代码,因为修改函数之中使用了与查询函数相同的代码。现在我可以对修改函数实施Substitute Algorithm(替换算法) ,设法让它再简洁一些:

  void sendAlert(String[] people){
      if (! foundPerson(people).equals(""))
          sendAlert();
  }
并发(Concurrency)问题

如果你在一个多线程系统中工作,肯定知道这样一个重要的惯用手法:
在同一个动作中完成检查和赋值。
这是否和单独查询互相矛盾呢? 我曾经和Doug Lea 讨论过这个问题,并得出结论:两者并不矛盾,但你需要做一 些额外工作。将查询动作和修改动作分开来仍然是很有价值的。但你需要保留第三个函数来同时做这两件事。这个「查询-修改」函数将调用各自独立的查询函数和 修改函数,并被声明为synchronized(同步) 时。
如果查询函数和修改函数未被声明为synchronized ,那么你还应该将它们的可见范围限制在package 级别或private 级别。这样,你就可以拥有一个安全、同步的操作,它由两个较易理解的函数组成。 这两个较低层函数也可以用于其他场合。

Parameterize Method(令函数携带参数)

问题:若干函数做了类似的工作,但在函数本体中却包含了不同的值
解决:建立单一函数,以参数表达那些不同的值。

//提高50%和提高10%   ==>  //提高(百分比)

clipboard.png

动机

你可能会发现这样的两个函数:它们做着类似的工作,但因少数几个值致使动作略有不同。这种情况下,你可以将这些各自分离的函数替换为一个统一函数,并通过参数来处理那些变化情况,用以简化问题。这样的修改可以去除重复的代码,并提高灵活性,因为你可以用这个参数处理其他(更多种)变化情况。

作法

  • 新建一个带有参数的函数,使它可以替换先前所有的重复性函数(repetitive methods)。
  • 将「对旧函数的调用动作」替换为「对新函数的调用动作」。
  • 对所有旧函数重复上述步骤,每次替换后,修改并测试。

范例

下面是一个最简单的例子:

class Employee {
    //提高10%
   void tenPercentRaise () {
       salary *= 1.1;
}
    //提高50%
   void fivePercentRaise () {
       salary *= 1.05;
}

这段代码可以替换如下:

//提高
void raise (double factor) {
   salary *= (1 + factor);
}

Replace Parameter with Explicit Methods(以明确函数取代参数)

问题:你有一个函数,其内完全取决于参数值而采取不同反应
解决:针对该参数的每一个可能值,建立一个独立函数

   //重构前
  void setValue (String name, int value) {
      if (name.equals("height"))
          _height = value;
      if (name.equals("width"))
          _width = value;
      //应该从未来过
      Assert.shouldNeverReachHere();
  }
   //重构后
  void setHeight(int arg) {
      _height = arg;
  }
  void setWidth (int arg) {
      _width = arg;
  }

动机

Replace Parameter with Explicit Methods洽恰相反于Parameterize Method。
如果某个参数有离散取值,而函数内又以条件式检查这些参数值,并根据不同参数值做出不同的反应,那么就应该使用本项重构。
调用者原本必须赋予参数适当的值,以决定该函数做出何种响应;
现在,既然你提供了不同的函数给调用 者使用,就可以避免出现条件式。

但是,如果参数值不会对函数行为有太多影响,你就不应该使用Replace Parameter with Explicit Methods。如果情况真是这样,而你也只需要通过参数为一个值域赋值,那么直接使用设值函数(setter)就行了。如果你的确需要「条件判断」 式的行为,可考虑使用Replace Conditional with Polymorphism。

作法

  • 针对参数的每一种可能值,新建一个明确函数。
  • 修改条件式的每个分支,使其调用合适的新函数。
  • 修改每个分支后,编译并测试。
  • 修改原函数的每一个被调用点,改而调用上述的某个合适的新函数。
  • 编译,测试。
  • 所有调用端都修改完毕后,删除原(带有条件判断的)函数

范例

下列代码中,我想根据不同的参数值,建立Employee 之下不同的subclass。以下 代码往往是Replace Constructor with Factory Method 的施行成果:

//工程师
 static final int ENGINEER = 0;
//推销员
  static final int SALESMAN = 1;
//经理
  static final int MANAGER = 2;
  static Employee create(int type) {
      switch (type) {
          case ENGINEER:
             return new Engineer();
          case SALESMAN:
             return new Salesman();
          case MANAGER:
             return new Manager();
          default:
             throw new IllegalArgumentException("Incorrect type code value");
      }
  }

由于这是一个factory method,我不能实施Replace Conditional with Polymorphism ,因为使用该函数时我根本尚未创建出对象。我并不期待太多新的subclasses,所以一个明确的接口是合理的(译注:不甚理解作者文意)。首先,我要根据参数值建立相应的新函数:

  static Employee createEngineer() {
      return new Engineer();
  }
  static Employee createSalesman() {
      return new Salesman();
  }
  static Employee createManager() {
      return new Manager();
  }

然后把「switch 语句的各个分支」替换为「对新函数的调用」:

  static Employee create(int type) {
      switch (type) {
          case ENGINEER:
             return Employee.createEngineer();
          case SALESMAN:
             return Employee.createSalesman();
          case MANAGER:
             return Employee.createManager();
          default:
             throw new IllegalArgumentException("Incorrect type code value");
      }
  }

接下来,我把注意力转移到旧函数的调用端。我把诸如下面这样的代码:
Employee kent = Employee.create(ENGINEER)
替换为:
Employee kent = Employee.createEngineer()
修改完create() 函数的所有调用者之后,我就可以把create() 函数删掉了。同时也可以把所有常量都删掉。

Preserve Whole Object(保持对象完整)

问题:你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
解决:改使用(传递)整个对象

//重构前
     int low = daysTempRange().getLow();
     int high = daysTempRange().getHigh();
     withinPlan = plan.withinRange(low, high);
//重构后
withinPlan = plan.withinRange(daysTempRange());

动机

有时候,你会将来自同一对象的若干项数据作为参数,传递给某个函数。

除了可以使参数列更稳固(不变动)之外,Preserve Whole Object 往往还能提高代码的可读性。
过长的参数列很难使用,因为调用者和被调用者都必须记住这些参数的用途。此外,不使用完整对象也会造成重复代码,因为被调用函数无法利用完整对象中的函数来计算某些中间值。
「甘蔗不曾两头甜」!如果你传的是数值,被调用函数就只与这些数值有依存关系(dependency),与这些数值所属对象没有任何依存关系。但如果你传递的是整个对象,「参数对象」和「被调用函数所在对象」之间,就有了依存关系。
如果这会使你的依存结构恶化,那么你就不该使用Preserve Whole Object。

我还听过另一种不使用Preserve Whole Object 的理由:
如果被调用函数只需要「参数对象」的其中一项数值,那么只传递那个数值会更好。
我并不认同这种观点,因为传递一项数值和传递一个对象,至少在代码清晰度上是等价的〔当然对于pass by value(传值)参数来说,性能上可能有所差异)。更重要的考量应该放在「对象之间的依存关系」上。

还有一种常见情况:调用者将自己的若干数据作为参数,传递给被调用函数。这种情况下,如果该对象有合适的取值函数(getter),你可以使用取代这些参数值,并且无须操心对象依存问题。

做法

  • 对你的目标函数新添一个参数项,用以代表原数据所在的完整对象。
  • 编译,测试。
  • 判断哪些参数可被包含在新添的完整对象中。
  • 选择上述参数之一,将「被调用函数」内对该参数的各个引用,替换为「对新添之参数对象的相应取值函数(getter)」的调用。
  • 删除该项参数。
  • 编译,测试。
  • 针对所有「可从完整对象中获得」的参数,重复上述过程。
  • 删除调用端中那些带有「被删除之参数」的所有代码。
  • 当然,如果调用端还在其他地方使用了这些参数,就不要删除它们。

范例

以下范例,我以一个Room 对象表示「房间」,它负责记录房间一天中的最高温度和最低温度。然后这个对象需要将「实际温度范围」与预先规定的「温度控制计划」 相比较,告诉客户当天温度是否符合计划要求:

//房间
class Room...
   boolean withinPlan(HeatingPlan plan) {
       int low = daysTempRange().getLow();
       int high = daysTempRange().getHigh();
       return plan.withinRange(low, high);
   }
//加热
class HeatingPlan...
    //范围内
   boolean withinRange (int low, int high) {
       return (low >= _range.getLow() && high <= _range.getHigh());
   }
//范围
   private TempRange _range;

其实我不必将TempRange 对象的信息拆开来单独传递,只需将整个对象传递给withinPlan() 函数即可。在这个简单的例子中,我可以一次性完成修改。如果相关的参数更多些,我也可以进行小步重构。首先,我为参数列添加新的参数项,用 以传递完整的TempRange 对象:

class HeatingPlan...
   boolean withinRange (TempRange roomRange) {
       return (roomRange.getLow() >= _range.getLow() && roomRange.getHigh() <= _range.getHigh());
   }
class Room...
   boolean withinPlan(HeatingPlan plan) {
       int low = daysTempRange().getLow();
       int high = daysTempRange().getHigh();
       return plan.withinRange(daysTempRange());
   }

现在,我不再需要low 和high 这两个临时变量了:

class Room...
   boolean withinPlan(HeatingPlan plan) {
       int low = daysTempRange().getLow();
       int high = daysTempRange().getHigh();
       return plan.withinRange(daysTempRange());
   }

使用完整对象后不久,你就会发现,可以将某些函数移到TempRange 对象中,使它更容易被使用,例如:

class HeatingPlan...
   boolean withinRange (TempRange roomRange) {
       return (_range.includes(roomRange));
   }
class TempRange...
   boolean includes (TempRange arg) {
       return arg.getLow() >= this.getLow() && arg.getHigh() <= this.getHigh();

Replace Parameter with Method(以函数取代参数)

问题:对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数也可以(也有能力)调用前一个函数。
解决:让参数接受者去除该项参数,并直接调用前一个函数。

    //重构前
     //需求量*单价=价格
     int basePrice = _quantity * _itemPrice;
     //获取优惠程度
     discountLevel = getDiscountLevel();
     //最终价格
     double finalPrice = discountedPrice (basePrice, discountLevel);
     //重构后
     int basePrice = _quantity * _itemPrice;
     double finalPrice = discountedPrice (basePrice);

动机

如果函数可以通过其他途径(而非参数列〕获得参数值,那么它就不应该通过参数取得该值。过长的参数列会增加程序阅读者的理解难度,因此我们应该尽可能缩短参数列的长度。

有时候,参数的存在是为了将来的弹性。这种情况下我仍然会把这种多余参数拿掉。是的,你应该只在必要关头才添加参数,预先添加的参数很可能并不是你所需要的。
不过,对于这条规则,也有一个例外:如果修改接口会对整个程序造成非常痛苦的结果(例如需要很长时间来重建程序,或需要修改大量代码〕,那么可以考虑保留前人预先加入的参数。如果真是这样,你应该首先判断修改接口究竟会造成多严重的后果,然后考虑是否「降低系统各部位之间的依存程度」以减少「修改接口所造成的影响」。稳定的接口确实很好,但是被冻结在一个不良接口上,也是有问题的。

作法

  • 如果有必要,将参数的计算过程提炼到一个独立函数中。
  • 将函数本体内「对该参数的引用」替换为「对新建函数的调用」。
  • 每次替换后,修改并测试。
  • 全部替换完成后,使用Remove Parameter 将该参数去掉。

范例:

以下代码用于计算定单折扣价格。虽然这么低的折扣不大可能出现在现实生活中, 不过作为一个范例,我们暂不考虑这一点:

public double getPrice() {
      int basePrice = _quantity * _itemPrice;
      int discountLevel;
      if (_quantity > 100) discountLevel = 2;
      else discountLevel = 1;
      double finalPrice = discountedPrice (basePrice, discountLevel);
      return finalPrice;
  }
  private double discountedPrice (int basePrice, int discountLevel) {
      if (discountLevel == 2) return basePrice * 0.1;
      else return basePrice * 0.05;
  }

首先,我把计算折扣等级(discountLevel)的代码提炼成为一个独立的 getDiscountLevel() 函数:

  public double getPrice() {
      int basePrice = _quantity * _itemPrice;
      int discountLevel = getDiscountLevel();
      double finalPrice = discountedPrice (basePrice, discountLevel);
      return finalPrice;
  }
  private int getDiscountLevel() {
      if (_quantity > 100) return 2;
      else return 1;
  }

然后把discountedPrice() 函数中对discountLevel 参数的所有引用点,替换为getDiscountLevel() 函数的调用:

  private double discountedPrice (int basePrice, int discountLevel) {
      if (getDiscountLevel() == 2) return basePrice * 0.1;
      else return basePrice * 0.05;
  }

此时我就可以使用Remove Parameter 去掉discountLevel 参数了 :

public double getPrice() {
      int basePrice = _quantity * _itemPrice;
      int discountLevel = getDiscountLevel();
      double finalPrice = discountedPrice (basePrice);
      return finalPrice;
  }
  private double discountedPrice (int basePrice) {
      if (getDiscountLevel() == 2) return basePrice * 0.1;
      else return basePrice * 0.05;
  }

接下来可以将discountLevel 变量去除掉:

  public double getPrice() {
      int basePrice = _quantity * _itemPrice;
      double finalPrice = discountedPrice (basePrice);
      return finalPrice;
  }

现在,可以去掉其他非必要的参数和相应的临时变量。最后获得以下代码:

  public double getPrice() {
      return discountedPrice ();
  }
  private double discountedPrice () {
      if (getDiscountLevel() == 2) return getBasePrice() * 0.1;
      else return getBasePrice() * 0.05;
  }
  private double getBasePrice() {
      return _quantity * _itemPrice;
  }

最后我还可以针对discountedPrice() 函数使用Inline Method:

  private double getPrice () {
      if (getDiscountLevel() == 2) return getBasePrice() * 0.1;
      else return getBasePrice() * 0.05;

Introduce Parameter Object(引入参数对象)

问题:某些参数总是很自然地同时出现。
解决:以一个对象取代这些参数

clipboard.png

动机

你常会看到特定的一组参数总是一起被传递。可能有好几个函数都使用这一组参数,这些函数可能隶属同一个class,也可能隶属不同的classes 。这样一组参数就是所谓的Date Clump (数据泥团)」,我们可以运用一个对象包装所有这些数据,再以该对象取代它们。哪怕只是为了把这些数据组织在一起,这样做也是值得的。本项重构的价值在于「缩短了参数列的长度」,而你知道,过长的参数列总是难以理解的。此外,新对象所定义的访问函数(accessors)还可以使代码更具一致性,这又进一步降低了代码的理解难度和修改难度。

本项重构还可以带给你更多好处。当你把这些参数组织到一起之后,往往很快可以发现一些「可被移至新建class」的行为。通常,原本使用那些参数的函数对那些参数会有一些共通措施,如果将这些共通行为移到新对象中,你可以减少很多重复代码。

作法

  • 新建一个class,用以表现你想替换的一组参数。将这个设为不可变的(不可被修改的,immutable)。
  • 针对使用该组参数的所有函数,实施Add Parameter,以上述新建class 之实体对象作为新添参数,并将此一参数值设为null 。
  • 对于Data Clump(数据泥团)中的每一项(在此均为参数),从函数签名式(signature)中移除之,并修改调用端和函数本体,令它们都改而通过「新建 的参数对象」取得该值。
  • 被搬移的可能是整个函数,也可能是函数中的一个段落。如果是后者, 首先使用Extract Method 将该段落提炼为一个独立函数,再搬移这一新建函数。

Remove Setting Method(移除设值函数)

问题:你的class 中的某个值域,应该在对象初创时被设值,然后就不再改变。
解决:去掉该值域的所有设值函数(setter)
clipboard.png

如果你为某个值域提供了设值函数(setter),这就暗示这个值域值可以被改变。如果你不希望在对象初创之后此值域还有机会被改变,那就不要为它提供设值函数 (同时并将该值域设为final )。这样你的意图会更加清晰,并且往往可以排除其值被修改的可能性——这种可能性往往是非常大的。
如果你保留了间接访问变量的方法,就可能经常有程序员盲目使用它们[Beck]。这些人甚至会在构造函数中使用设值函数!我猜想他们或许是为了代码的一致性,但却忽视了设值函数往后可能带来的混淆。

作法

  • 检查设值函数(setter)被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用。
  • 修改构造函数,使其直接访问设值函数所针对的那个变量。
  • 如果某个subclass 通过设值函数给superclass 的某个private 值域设了值,那么你就不能这样修改。这种情况下你应该试着在superclass 中提供一个protected 函数(最好是构造函数)来给这些值域设值。不论你怎么做,都不要给superclass 中的函数起一个与设值函数混淆的名字。
  • 编译,测试。
  • 移除这个设值函数,将它所计对的值域设为final 。

单例子:

//重构前
class Account {
  private String _id;
  Account (String id) {
      setId(id);
  }
  void setId (String arg) {
     _id = arg;
  }
//重构后
class Account {
  private final String _id;
  Account (String id) {
     _id = id;
  }

Hide Method(隐藏某个函数)

问题:有一个函数,从来没有被其他任何class 用到。
解决:将这个函数修改为private 。

clipboard.png

动机

重构往往促使你修改「函数的可见度」( visibility of methods)。提高函数可见度的情况很容易想像:另一个class 需要用到某个函数,因此你必须提高该函数的可见度。但是要指出一个函数的可见度是否过高,就稍微困难一些。理想状况下你可以使用工具检查所有函数,指出可被隐藏起来的函数。即使没有这样的工具,你也应该时常进行这样的检查。
一种特别常见的情况是:当你而对一个过于丰富、提供了过多行为的接口时,就值得将非必要的取值函数(getter)和设值函数(setter)隐藏起来。尤其当你面对的是一个「只不过做了点简单封装」的数据容器(data holder)时,情况更是如此。 随着愈来愈多行为被放入这个class 之中,你会发现许多取值/设值函数不再需要为public ,因此可以把它们隐藏起来。如果你把取值/设值函数设为private ,并在他处直接访问变量,那就可以放心移除取值/设值函数了。

做法

  • 经常检查有没有可能降低某个函数的可见度(使它更私有化)。
  • 特别对设值函数(setter)进行上述的检查。
  • 尽可能降低所有函数的可见度。
  • 每完成一组函数的隐藏之后,编译并测试。
  • 如果有不适当的隐藏,编译器很自然会检验出来,因此不必每次修改 后都进行编译。如有任何错误出现,很容易被发现。

Replace Constructor with Factory Method(以「工厂函数」取代「构造函数」)

问题:你希望在创建对象时不仅仅是对它做简单的建构动作(simple construction )

解决:将constructor (构造函数)替换为factory method(工厂函数)。

//重构后
Employee (int type) {
    _type = type;
}
//重构后
static Employee create(int type) {
    return new Employee(type);
}

动机

使用Replace Constructor with Factory Method 的最显而易见的动机就是在subclassing(子类化) 过程中以factory method 以取代type code(类型代码)。
你可能常常需要根据type code 创建相应的对象,现在,创建名单中还得加上subclasses,那些subclasses 也是根据type code 来创建。然而由于构造函数只能返回「被索求之对象」,因此你需要将构造函数替换为Factory Method [Gang of Four]。
此外,如果构造函数的功能不能满足你的需要,也可以使用factory method 来代替它。
Factory method 也是Change Value to Reference(将值更改为引用) 的基础。你也可以令你的factory method 根据参数的个数和型别,选择不同的创建行为。

作法

  • 新建一个factory method ,让它调用现有的构造函数。
  • 将「对构造函数的调用」替换为「对factory method 的调用」。
  • 每次替换后,编译并测试。
  • 将构造函数声明为private。
  • 编译。

范例:

又是那个单调乏味的例子:员工薪资系统。我以Employee 表示「员工」:

class Employee {
  private int _type;
  static final int ENGINEER = 0;
  static final int SALESMAN = 1;
  static final int MANAGER = 2;
  Employee (int type) {
      _type = type;
  }

我希望为Employee 提供不同的subclasses,并分别给予它们相应的type code。因此,我需要建立一个factory method :

static Employee create(int type) {
     return new Employee(type);
  }

然后,我要修改构造函数的所有调用点,让它们改用上述新建的factory method , 并将构造函数声明为private

client code...
  Employee eng = Employee.create(Employee.ENGINEER);
class Employee...
  private Employee (int type) {
     _type = type;
  }

范例2:

根据字符串(String)创建subclass 对象

迄今为止,我还没有获得什么实质收获。
目前的好处在于:我把「对象创建之调用 动作的接收者」和「被创建之对象所属的class 」分开了。
如果我随后使用Replace Type Code with Subclasses(以子类取代类型码) 把type code 转换为Employee 的subclass ,我就可以运用factory method ,将这些subclass 对用户隐藏起来:

static Employee create(int type) {
      switch (type) {
          case ENGINEER:
             return new Engineer();
          case SALESMAN:
             return new Salesman();
          case MANAGER:
             return new Manager();
          default:
             throw new IllegalArgumentException("Incorrect type code value");
      }
  }

可惜的是,这里面有一个switch 语句。如果我添加一个新的subclass ,就必须记得更新这里的switch 语句,而我又偏偏很健忘。

绕过这个switch 语句的一个好办法是使用Class.forName() 。
第一件要做的事是修改参数型别,这从根本上说是Rename Method 的一种变体。首先我得建立一个函数,让它接收一个字符串引数(string argument):

 static Employee create (String name) {
      try {
          return (Employee) Class.forName(name).newInstance();
      } catch (Exception e) {
          throw new IllegalArgumentException ("Unable to instantiate" + name);
      }
  }

然后让稍早那个「create() 函数int 版」调用新建的「create() 函数String 版」:

class Employee {
  static Employee create(int type) {
      switch (type) {
          case ENGINEER:
             return create("Engineer");
          case SALESMAN:
             return create("Salesman");
          case MANAGER:
             return create("Manager");
          default:
             throw new IllegalArgumentException("Incorrect type code value");
      }
  }

然后,我得修改create() 函数的调用者,将下列这样的语句:

Employee.create(ENGINEER)

修改为:

Employee.create("Engineer")


完成之后,我就可以将「create() 函数,int 版本」移除了。
现在,当我需要添加新的Employee subclasses,就不再需要更新create() 函数了。 但我却因此失去了编译期检验,使得一个小小的拼写错误就可能造成运行期错误。如果有必要防止运行期错误,我会使用明确函数来创建对象(见本页下)。
但这样一来,每添加一个新的subclass ,我就必须添加一个新函数。这就是为了型别安全而牺牲掉的灵活性。

另一个「必须谨慎使用(Class.forName() 」的原因是:它向用户暴露了subclass 名称。不过这并不是太糟糕,因为你可以使用其他字符串,并在factory method 中执行其他行为。这也是「不使用Inline Method 去除factory method 的一个好理由。

范例:

我可以通过另一条途径来隐藏subclass ——使用明确函数。
如果你只有少数几个subclasses,而且它们都不再变化,这条途径是很有用的。
我可能有个抽象的Person class(人类),它有两个subclass :Male 和Female(男性和女性)。首先我在superclass 中为每个subclass 定义一个factory method :

class Person...
  static Person createMale(){
      return new Male();
  }
  static Person createFemale() {
      return new Female();
  }

然后我可以把下面的调用:
Person kent = new Male();
替换成:
Person kent = Person.createMale();
但是这就使得superclass 必须知晓subclass 。如果想避免这种情况,你需要一个更为复杂的设计,例如 Product Trader 模式[Bäumer and Riehle]。绝大多数情况下你并不需要如此复杂的设计,上面介绍的作法已经绰绰有余。

Encapsulate Downcast(封装「向下转型」动作)

问题:某个函数返回的对象,需要由函数调用者执行「向下转型」(downcast)动作。

解决:将向下转型(downcast)动作移到函数中。

//重构前
Object lastReading() {
  return readings.lastElement();
}
//重构后
Reading lastReading() {
  return (Reading) readings.lastElement();
}

动机

在强型别(strongly typed)OO语言中,向下转型是最烦人的事情之一。
之所以很烦人,是因为从感觉上来说它完全没有必要:你竟然越俎代庖地告诉编译器某些应该由编译器自己计算出来的东西。
但是,由于「计算对象型别」的动作往往比较麻烦,你还是常常需要亲自告诉编译器「对象的确切型别」。向下转型在Java 特别盛行,因为Java 没有template(模板)机制,因此如果你想从群集(collection)之中取出一个对象,就必须进行向下转型。
向下转型也许是一种无法避免的罪恶,但你仍然应该尽可能少做。如果你的某个函数返回一个值,并且你知道「你所返回的对象」其型别比函数签名式(signature) 所昭告的更特化(specialized;译注:意指返回的是原本声明之return type 的subtype),你便是在函数用户身上强加了非必要的工作。这种情况下你不应该要求用户承担向 下转型的责任,应该尽量为他们提供准确的型别。

《越俎代庖》(yuè zǔ dài páo):越:跨过;俎:古代祭祀时摆祭品的礼器;庖:厨师。主祭的人跨过礼器去代替厨师办席。比喻超出自己业务范围去处理别人所管的事。

强类型语言中,所有基本数据类型(整型,字符型,浮点型等等)都作为该语言的一部分被预定义。程序中的所有变量和常量都必须借助这些数据类型来表示。对数据可执行的操作也因数据类型的不同而异。
强类型语言的编译器强制使用数据类型并要求操作对数据类型合法(比如加法对整型合法,取余对浮点型不合法)。
强类型语言的一个优点是强迫编程者遵循严谨的、统一的数据使用规范,降低出错风险。缺点是降低了编程的灵活性和创造性,这体现在:编程者被局限于语言发明者提供的数据类型;对已有的数据类型,编程者所能执行的操作被限制。

作法

  • 找出「必须对函数调用结果进行向下转型」的地方。
  • 这种情况通常出现在「返回一个群集(collection)或迭代器(iterator)」 的函数中。
  • 将向下转型动作搬移到该函数中。
  • 针对返回群集(collection)的函数,使用Encapsulate Collection 。

范例:

下面的例子中,我以Reading 表示「书籍」。我还拥有一个名为lastReading() 的函数,它从一个用以「保存Reading 对象」的Vector 中返回其最后一个元素:

Object lastReading() {
  return readings.lastElement();
}

我应该将这个函数变成:

Reading lastReading() {
  return (Reading) readings.lastElement();
}

当我拥有一个群集时,上述那么做就很有意义。如果「保存Reading 对象」的群集被放在Site class (网站类)中,并且我看到了如下的代码(客户端):

Reading lastReading = (Reading) theSite.readings().lastElement()

我就可以不再把「向下转型」工作推给用户,并得以向用户隐藏群集:

Reading lastReading = theSite.lastReading();
class Site...
   Reading lastReading() {
       return (Reading) readings().lastElement();
   }

Replace Error Code with Exception(用异常取代错误码)

问题:某个函数返回一个特定的代码(special code),用以表示某种错误情况。

解决:改用异常(exception)

//重构前
//撤退
int withdraw(int amount) {
    if (amount > _balance)//数量大于平衡值
        return -1;
    else {
        _balance -= amount;
        return 0;
    }
}
//重构后
void withdraw(int amount) throws BalanceException {
    if (amount > _balance) throw new BalanceException();
    _balance -= amount;
}

动机

和生活一样,计算器偶尔也会出错。一旦事情出错,你就需要有些对策。
最简单的情况下,你可以停止程序运行,返回一个错误码。

如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以比较认真的方式来处理。
问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段副程序 (routine)发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着调用链(call chain)传递上去。
许多程序都使用特殊输出来表示错误,Unix 系统和C-based 系统的传统方式就是「以返回值表示副程序的成功或失败」。
Java 有一种更好的错误处理方式:异常(exceptions)。这种方式之所以更好,因 为它清楚地将「普通程序」和「错误处理」分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标

从系统的来源角度:Linux是Unix的开源实现,Unix一开始的时候也是开源的,后来才以授权方式闭源,这时候在原始Unix基础上诞生了AIX,HP-UX等这些都是正统Unix,但BSD被认为是Unix精神的继承者(BSD同样也是Unix的另外一个开源实现)也是Unix的一种,Linux为什么比BSD发展的好这里不讨论。从这里看Linux是Unix的一个模仿者之一

作法

  • 决定待抛异常应该是checked 还是unchecked。
  • 如果调用者有责任在调用前检查必要状态,就抛出unchecked异常。
  • 如果想抛出checked 异常,你可以新建一个exception class,也可以使用现有的exception classes。
  • 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。
  • 如果函数抛出unchecked 异常,那么就调整调用者,使其在调用函数 前做适当检查。每次修改后,编译并测试。
  • 如果函数抛出checked 异常,那么就调整调用者,使其在try 区段中调用该函数。
  • 修改该函数的签名式(sigature),令它反映出新用法。

范例:

现实生活中你可以透支你的账户余额,计算器教科书却总是假设你不能这样做,这不是很奇怪吗?不过下而的例子仍然假设你不能这样做:

class Account...
 private int _balance;

  int withdraw(int amount) {
      if (amount > _balance)
          return -1;
      else {
          _balance -= amount;
          return 0;
      }
  }

为了让这段代码使用异常,我首先需要决定使用checked 异常还是unchecked 异常。
决策关键在于:调用者是否有责任在取款之前检查存款余额,或者是否应该由 withdraw() 函数负责检查。如果「检查余额」是调用者的责任,那么「取款金额大于存款余额」就是一个编程错误。
由于这是一个bug, 所以我应该使用unchecked 异常。
另一方面,如果「检查余额」是withdraw() 函数的责任,我就必须在函数接口中声明它可能抛出这个异常(译注:这是一个checked 异常),那么也就提醒了调用者注意这个异常,并采取相应措施。

范例2:unchecked 异常

首先考虑unchecked 异常。使用这个东西就表示应该由调用者负责检查。首先我需要检查调用端的代码,它不应该使用withdraw() 函数的返回值,因为该返回值只用来指出程序员的错误。如果我看到下面这样的代码:

      if (account.withdraw(amount) == -1)
          handleOverdrawn();//处理透支
      else doTheUsualThing();//正常处理

我应该将它替换为这样的代码:

    if (!account.canWithdraw(amount))
          handleOverdrawn();
      else {
          account.withdraw(amount);
          doTheUsualThing();
      }

每次修改后,编译并测试。
现在,我需要移除错误码,并在程序出错时抛出异常。由于行为(根据其文本定义 得知)是异常的、罕见的,所以我应该用一个卫语句(guard clause)检查这种情况:

  void withdraw(int amount) {
      if (amount > _balance)
          throw new IllegalArgumentException ("Amount too large");
      _balance -= amount;
  }

由于这是程序员所犯的错误,所以我应该使用assertion(断言) 更清楚地指出这一点:

class Account...
  void withdraw(int amount) {
      Assert.isTrue ("amount too large", amount > _balance);
      _balance -= amount;
  }
class Assert...
  static void isTrue (String comment, boolean test) {
      if (! test) {
          throw new RuntimeException ("Assertion failed: " + comment);
      }
  }

范例:checked 异常

checked 异常的处理方式略有不同。首先我要建立(或使用)一个合适的异常:

class BalanceException extends Exception {}

然后,调整调用端如下:

   try {
         account.withdraw(amount);
         doTheUsualThing();
     } catch (BalanceException e) {
         handleOverdrawn();
     }

接下来我要修改withdraw() 函数,让它以异常表示错误状况:

 void withdraw(int amount) throws BalanceException {
      if (amount > _balance) throw new BalanceException();
      _balance -= amount;
  }

这个过程的麻烦在于:我必须一次性修改所有调用者和被它们调用的函数,否则编译器会报错。如果调用者很多,这个步骤就实在太大了,其中没有编译和测试的保障。
这种情况下,我可以借助一个临时中间函数。我仍然从先前相同的情况出发:

if (account.withdraw(amount) == -1)
     handleOverdrawn();
else doTheUsualThing();
class Account ...
int withdraw(int amount) {
     if (amount > _balance)
         return -1;
     else {
         _balance -= amount;
         return 0;
     }
  }

首先,产生一个newWithdraw() 函数,让它抛出异常:

  void newWithdraw(int amount) throws BalanceException {
      if (amount > _balance) throw new BalanceException();
      _balance -= amount;
  }

然后,调整现有的withdraw() 函数,让它调用newWithdraw() :

  int withdraw(int amount) {
      try {
          newWithdraw(amount);
          return 0;
      } catch (BalanceException e) {
          return -1;
      }
  }

完成以后,编译并测试。现在我可以逐一将「对旧函数的调用」替换为「对新函数 的调用」:

   try {
          account.newWithdraw(amount);
          doTheUsualThing();
      } catch (BalanceException e) {
          handleOverdrawn();
      }

由于新旧两函数都存在,所以每次修改后我都可以编译、测试。所有调用者都被我修改完毕后,旧函数便可移除,并使用Rename Method 修改新函数名称,使它与旧函数相同。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值