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