重构函数调用-Replace Error Code with Exception用异常取代错误码十四

Source

重构函数调用-Replace Error Code with Exception用异常取代错误码十四

1.用异常取代错误码

1.1.使用场景

某个函数返回一个特定的代码,用以表示某种错误情况。改用异常。

和生活一样,计算机偶尔也会出错。一旦事情出错,你就需要有些对策。最简单的情况下,你可以停止程序运行,返回一个错误码。这就好像因为错过一班飞机而自杀一样(如果真那么做,哪怕我是只猫,我的九条命也早赔光了)。尽管我的油腔滑调企图带来一点幽默,但这种“软件自杀”选择的确是有好处的。如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以更认真的方式来处理。

问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段子程序发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着调用链传递上去。许多程序都使用特殊输出来表示错误,Unix系统和C-based系统的传统方式就是以返回值表示子程序的成功或失败。
Java有一种更好的错误处理方式:异常。这种方式之所以更好,因为它清楚地将“普通程序”和“错误处理”分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。

1.2.如何做

  • 决定应该抛出受控(checked)异常还是非受控(unchecked)异常。
  • 如果调用者有责任在调用前检查必要状态,就抛出非受控异常。
  • 如果想抛出受控异常,你可以新建一个异常类,也可以使用现有的异常类。
  • 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。
  • 如果函数抛出非受控异常,那么就调整调用者,使其在调用函数前做适当检查。每次修改后,编译并测试。
  • 如果函数抛出受控异常,那么就调整调用者,使其在try区段中调用该函数。
  • 修改该函数的签名,令它反映出新用法。
  • 如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤。
  • 决定应该抛出受控异常还是非受控异常。
  • 新建一个函数,使用异常来表示错误状况,将旧函数的代码复制到新函数中,并做适当调整。
  • 修改旧函数的函数本体,让它调用上述新建函数。
  • 编译,测试。
  • 逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。
  • 移除旧函数。

1.3.示例

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

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

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

非受控异常

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

if (account.withdraw(amount) == -1)
    handleOverdrawn();
else doTheUsualThing();

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

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

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

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

受控异常

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

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 修改新函数名称,使它与旧函数相同。