Replace Error Code with Exception (以异常取代错误码)
- 摘要:《重构:改善既有代码的设计》清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。第10章讲述简化函数调用。本节说的是Replace Error Code with Exception (以异常取代错误码)
- 标签:重构 代码 函数 重构:改善既有代码的设计
10.14 Replace Error Code with 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;
- }
动机
和生活一样,计算机偶尔也会出错。一旦事情出错,你就需要有些对策。最简单的情况下,你可以停止程序运行,返回一个错误码。这就好像因为错过一班飞机而自杀一样(如果真那么做,哪怕我是只猫,我的九条命也早赔光了)。尽管我的油腔滑调企图带来一点幽默,但这种"软件自杀"选择的确是有好处的。如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以更认真的方式来处理。
问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段子程序发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这个错误继续沿着调用链传递上去。许多程序都使用特殊输出来表示错误,Unix系统和C-based系统的传统方式就是以返回值表示子程序的成功或失败。
Java有一种更好的错误处理方式:异常。这种方式之所以更好,因为它清楚地将"普通程序"和"错误处理"分开了,这使得程序更容易理解--我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。
做法
决定应该抛出受控(checked)异常还是非受控(unchecked)异常。
如果调用者有责任在调用前检查必要状态,就抛出非受控异常。
如果想抛出受控异常,你可以新建一个异常类,也可以使用现有的异常类。
找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。
如果函数抛出非受控异常,那么就调整调用者,使其在调用函数前做适当检查。每次修改后,编译并测试。
如果函数抛出受控异常,那么就调整调用者,使其在try区段中调用该函数。
修改该函数的签名,令它反映出新用法。
如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤。
决定应该抛出受控异常还是非受控异常。
新建一个函数,使用异常来表示错误状况,将旧函数的代码复制到新函数中,并做适当调整。
修改旧函数的函数本体,让它调用上述新建函数。
编译,测试。
逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。
移除旧函数。
范例
现实生活中你可以透支你的账户余额,计算机教科书却总是假设你不能这样做,这不是很奇怪吗?不过下面的例子仍然假设你不能这样做:
- 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;
- }
- 由于这是程序员所犯的错误,所以我应该使用断言更清楚地指出这一点:
- class Account...
- void withdraw(int amount) {
- Assert.isTrue("sufficient funds", 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 (273)修改新函数名称,使它与旧函数相同。