iT邦幫忙

2

例外處理(Exception Throwing)設計準則

這天小明問我說

我這個API 方法會回傳錯誤代碼, 呼叫端要處理這些各種不同的錯誤代碼, 
可以幫忙看看這些程式碼有沒有沒考慮到的地方?

許多人沒有使用異常處理的藉口有很多, 但是大多數歸結為兩種看法:

  • 異常處理語法是不可取的, 因此以某種方式返回錯誤代碼是比較好的做法.
  • "拋出異常"方式 的性能不如 "傳回錯誤代碼"方式

最好用 "例外" 取代 "回傳錯誤代碼"

我們看看下面的示範程式碼:

switch( checkLogin() ){
   case -1:
      //Invalid credentials
      ...
      break;
   case -2:
      //Too many login attempts
      ...
      break;
   default:
      // Successful
      break;
}

以上程式碼有兩個問題

  • 為了知道執行 checkLogin 方法的回傳值 "-1" 是什麼意思, 我必須看一下 checkLogin 方法的實作內容.
  • 更改錯誤代碼值怎麼辦? 我將必須檢查 checkLogin 方法的所有用法, 才能更改接收到的回傳值!

您可以採用另一種方​​法來解決前面所述的2個主要問題

switch( checkLogin() ){
   case ErrorCode.INVALID_LOGIN_CREDENTIALS:
      ...
      break;
   case ErrorCode.TOO_MANY_LOGIN_ATTEMPTS:
      ...
      break;
   default:
      // Successful scenario, log in the user
      break;
}

但是如果我們想在 checkLogin 方法中重構一個 Extract 方法, 那將是很困難的工作: 我們必須從 checkLogin 方法中攜帶錯誤代碼, 在 Extract 方法中有用到的錯誤代碼必須將其傳回去. 為了在我們的應用程序的外層中委派處理這種特殊情況的邏輯, 我們可能需要更大的靈活性.

例如以下 checkLogin 程式碼, 雖然用了 enum 方式取代了錯誤代碼的魔法數字

private ErrorCode checkLogin() {
   ...
   if ( hasNotValidCredentials ) {
      return ErrorCode.INVALID_LOGIN_CREDENTIALS;
   }

   ...
   if ( hasTooManyLoginAttempts ) {
      return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS;
   }

   return ErrorCode.LOGIN_SUCCESSFUL;
}

當我們想要嘗試 Extract 這一段程式碼

if ( hasTooManyLoginAttempts ) {
   return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS;
}

重構變成下面程式碼...

private ErrorCode Extract() {
  if ( hasTooManyLoginAttempts ) {
      return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS;
  }
  ???
}

進行到這裡, 你就會發現這還得花更大的力氣才能往下做...

考慮到前面描述的問題, 在這種情況下唯一可以幫助的是用 "丟例外錯誤" 替換 "錯誤代碼"

private void checkLogin() {
   ...
   if ( hasNotValidCredentials ) {
      throw new InvalidLoginCreadentialsException();
   }

   ...
   if ( hasTooManyLoginAttempts ) {
      return new TooManyLoginAtteptsException();
   }
}

然後你就可以輕鬆的進行重構(Refactoring), 變成下面程式碼

private void checkLogin()
{
   checkLoginCredentials();
   checkLoginAttempts();
   checkBannedUser();
}

在重構過程中(Refactoring), 完全根本不需要考慮回傳值的問題


如果將異常用於經常失敗的代碼, 則程式碼執行的性能將是不可接受的. 這是一個的確令人擔憂的問題. 當程式碼拋出異常時, 其性能可能會降低幾個數量級. 但是在嚴格遵守不允許使用錯誤代碼的例外處理準則的同時, 我們也可以獲得良好的性能. 有兩種建議可以解決這個問題.

測試者-執行者模式(Tester-Doer Pattern)

有時候將發生例外的方法內容可以拆成兩部分, 這可以提高其性能. 例如下面程式碼示範:

讓我們看一下Dictionary 類的indexed 屬性.

var table = new Dictionary<string,int>();
...
int value = table["key"];

如果table 字典中不存在該鍵值(Key), 則索引器將引發例外錯誤. 在這段程式碼經常執行失敗的情況下, 這會導致引起執行性能問題(Performance Problem). 緩解問題的方法之一是在訪問鍵值之前測試鍵是否在字典中.

var table = new Dictionary<string,int>();
...
if( table.Contains("key") ){
   int value = table["key"];
}

在上面的示例中, 包含條件的用於測試條件的成員稱為"測試者".

if( table.Contains("key") )

用於執行潛在發生例外的成員(索引器) 稱為"執行者".

int value = table["key"];

TryParse 模式(TryParse Pattern)

對於性能要求極高的API , 應使用Tester-Doer 更快的模式.

例如DateTime 定義了一個Parse 方法, 該方法在字符串解析失敗時拋出例外. 但它還定義了一個相應的TryParse 方法, 該方法嘗試進行解析, 但是如果解析失敗則返回false, 並使用out 參數返回成功解析的結果.

使用此模式時, 在"try" 的功能中, 如果嘗試了所有方法無效, 最後仍然失敗時, 則該方法仍必須拋出例外.


尚未有邦友留言

立即登入留言