l

2010年3月22日 星期一

敏捷式例外處理設計 (6):我到底哪裡做錯之 unprotected main program

03/22 20:12~22:21

以下的內容需要先閱讀『敏捷式例外處理設計的第一步:決定例外處理等級』比較容易理解。
 
今天 Teddy 回到例外處裡這個主題,要談的是 unprotected main program 這個 exception handling bad smell。如果鄉民們還記得『例外處裡強健性等級』,那麼應該知道你所開發的程式應該至少要達到 G1 (error-reporting) 等級:遇到例外直接往外丟,如果是 checked exceptions 則轉成 unchecked exceptions 往外丟。這下好了,大家都卯起來把例外往外丟,當這些未被捕捉 (uncaught) 的例外飄到程式最上層 (例如 main programthread,或是跨過某個 layer),可能會導致系統不預期的終止。由於此時系統的狀態可能已經不正確了,所以終止執行也是應該的。但是就算是十惡不赦的壞人,在臨死之前也應該給他個機會交代一下遺言。而 unprotected main program 的問題就是『沒有給程式在臨死前交代遺言的機會』。請看以下 Java 程式片段:

static public void main(String [] args){
// some code
}


程式遇到例外直接結束,將控制權回傳給作業系統。

***

Avoid unexpected termination with big outer try block


要移除 unprotected main program 可以套用 Avoid unexpected termination with big 
outer try block 這個 refactoring,請看:



static public void main(String [] args){
   
    // some code
}

變成



static public void main(String [] args){

     try {
           // some code
     } catch (Throwable e) {
          // displaying and/or looging the exception
     }
}

上面的例子看起來很簡單,當程式執行時也只有一個 main program,感覺Avoid unexpected termination with big outer try block 的應用好像很有限。其實這個 refactoring 可以應用在其他不同的地方,例如之前曾經提過的 Replace nested try block with method 這個 refactoring 的實做:

private void closeIO(Closeable c){

     try {
         if (in != null)
               in.close();
     }
     catch(IOException e)
     {
           // log the exception
     }
}

closeIO() 這個用來釋放 IO 資源的 method 內部就用一個 big outer try block 包起來,以防止 closeIO() 不正常終止。

***

鄉民甲:不是說程式狀態不對就應該要終止嗎?

Teddy:這裡的用法是一個特例,請參考 敏捷式例外處理設計 (4):我到底哪裡做錯之 nested try block

***

如果你的軟體架構採用 layer架構,當例外從底層的某個 layer 要傳遞到上層的 layer,此時也可以套用 Avoid unexpected termination with big outer try block 來把例外轉成對上層 layer 比較有意義的例外。看一下這個例子:假設你寫了一個 updateUser() method用來更新資料庫裡面的使用者資料,這個 method 會被 Web UI 呼叫。由於客戶要求每一筆資料修改都要紀錄之前的歷史資料,所以你寫了一個通用的 updateChangeHistoryInternal() method 來執行紀錄歷史資料的工作。你將更新使用者的實做寫在 updateUserInternal() method 裡面,然後把 updateUserInternal() updateChangeHistoryInternal() 包在 updateUser() 中,如下所示:

public void updateUser(User aUser){
   updateUserInternal(aUser);
      updateChangeHistoryInternal(aUser);
}

假設 updateUserInternal() updateChangeHistoryInternal() 會丟出下列幾個 unchecked exceptions: 

  • updateUserInternal: InvalidDataException, DuplicateKeyException 
  • updateChangeHistoryInternal: ConstraintViolationException

如果 updateUser() 執行發生錯誤,呼叫它的人可能會收到 InvalidDataException, DuplicateKeyException,或是ConstraintViolationException 。這三個例外可以被視為是『實做例外』(implementation exception),這些實做例外不應該直接傳給不同 layer 的其他程式。如果讓 caller 收到實做例外,那麼 caller 會面對下列幾個問題:


  • 太多實做例外對 caller 而言不容易捕捉並處裡。
  • 如果實做方式改變,那麼 caller 的例外處裡程式碼也需要隨著改變
 
請看下面範例程式碼:
 
public void caller(){
    
   User user = new User(...);
     try {
           updateUser(user);
     }
     catch(InvalidDataException e){
           // handler
     }
     catch(DuplicateKeyException e){
           // handler
    }
    catch(ConstrintViolationException e){
          // handler
    }
}

如果 updateUserInternal() 不再丟出 DuplicateKeyException 那麼原本寫在 caller() 裡面的例外處裡程式就用不到了。

所以,老招數拿出來用:

public void updateUser(User aUser){

     try {
         updateUserInternal(aUser);
         updateChangeHistoryInternal(aUser);
     }
     catch(Exception e){
         throw new DAOErrorException(“Update user error.”, e);
    }
}

updateUser() method 裡面加一個 big outer try block 也算是套用了Avoid unexpected termination with big outer try block。改完之後 caller 程式就變成:

public void caller(){
 
   User user = new User(...);
      try {
           updateUser(user);
      }
      catch(DAOErrorException e){
           // handler
     }
}
***

鄉民乙:你的資料庫程式範例怎麼都沒有作 transaction control

Teddy:你可以用 Spring AOP 來作 transaction control,這樣就不用直接再程式碼中寫 begin transaction, rollback 這類的控制。

***

友藏內心獨白:第一階段 exception handling bad smells 介紹完畢。

3 則留言:

  1. 話說這一篇應該是(六)吧

    而且哦 ....... 用AOP控制Transaction在實務上偶爾會發生令人"十分不快"的狀況

    回覆刪除
  2. 我應該頒發一個『忠實鄉民』獎給你...找到不少 bugs...

    用 APO 做 transaction 會有什麼問題?趕快說來聽聽?

    回覆刪除
  3. 你好,我不知道在哪邊有讀到,try...catch...的block內的程式不是應該要越少越好嗎?這種一整塊程式都用try..catch包起來的refactor不會導致效能比較差嗎?

    回覆刪除