21 July 2005

以下這些 practice 是在 struts+hibernate+spring 環境下琢磨出來的:

  • 將所有的 Business Checked Exception 皆繼承單一的 BusinessException。有一個 root exception 對 AOP (Spring) 來說很方便。比方說在 Spring 裡,Transaction rollback 預設的 rule 可以設為
    <property name="transactionAttributes">
        <props>
    	<prop key="save*">PROPAGATION_REQUIRED, -com.myapp.BusinessException</prop>
            ....
    如此只要有 method throw BusinessException 的 subclass 即可 rollback,而不用每個 service method 都特別去設定某某 check exception 要 rollback 了。

  • 請勿拿 exception 當做流程控制... 例如 A 段的 code 執行到一半出現 exception 後,就改做 B 段的 code... 這是錯誤的使用法。另外,如果你曾仔細觀察過,就會發現 exception 的速度是很慢的,拿exception 當 if 來用,真的會很慢...慢......

  • 當發生錯誤時,使用者如果無法處理,請將該錯誤設計成 RuntimeException。還有即然無法回復,就不該再去執行其他的程式碼,而是應該讓程式全部中斷 (頂多只執行 finally的 block) 然後丟到最外層。通常 RuntimeException 都是當作 constraint/assertion 來用 (即出現時表示程式有 bug),或是不可抗力因素,如外部環境出錯、連不上 database 等等。

  • 如果發生了使用者可回復的錯誤,就使用 BusinessException (checked exception) 的 subclass。然後傳給使用者,讓使用者來判斷在哪出錯,下一步該做什麼。以程式來看,就是 BusinessException 要一路 throw 到 Struts Action 那一層,再轉換為 message 讓使用者看。

    //下面例子的 AException 為一 BusinessException subclass
    
    //錯誤:錯把 exception 當流程控制
      public void wrong() {          
        try {                           
          logicA.execute() ;              
        } catch (AException e) { 
          //錯!         
          logicB.execute() ;              
          //...                              
        }                                 
      }                                 
    
    //正確 1:check exception 應 throw 出去給 Action 轉存為 message
    //讓使用者判斷
    public void right1() throw AException {
        logicA.execute() ;
    }
    
    
    //正確 2:如果真的遇到使用者無法處理的 check exception,那應當事先核對
    //logicA 是否合理,如果是則繼續執行。若核對過了還出現 AExcetion 
    //則改丟出 RuntimeException (表示程式中有某個 bug,這真的算是 "例外" 了)
    public void right2() {
       if(isLogicA_valid()) { //事先判斷 logicA 可否執行
         try {
            logicA.execute() ;
         } catch (AException e) {
            throw new UnexpectedRuntimeException(e) ;
         }
       } else {
            logicB.execute() ;
       }
    }

  • 請勿在 service layer 和 domain layer 中直接 catch 或 throw 最大的 Exception 或是 BusinessException:
    try {
             //.....
          } catch (Exception e) {
             //錯誤範例:catch 最大的 exception
          }   
    
          try {
             //.....
          } catch (BusinessException e) {
             //錯誤範例:catch 最上層的 exception
          }
    
          public void createSomething throws Exception {
              //錯誤範例:throw 最大的 exception
          }
    
          public void createSomething throws BusinessException {
              //錯誤範例:throw 最上層的 exception
          }
    catch/throws 最大的 exception 多半只會出現在 AOP/interceptor/proxy 之類的地方,business logic 裡 catch/throws exception 範圍越小越好,這樣才會清楚發生了什麼事情,而不會不心就 catch 到不該 catch 的 exception。

  • Struts Action 的 execute method 的 signature:
    public ActionForward execute(ActionMapping mapping, ActionForm form,
                HttpServletRequest request, HttpServletResponse response) 
          throws Exception {
          // 這個 Exception 害人不淺啊!!
       }
    如果用 IDE 產生這個 method,一不小心就會掛上這個最大的 Exception。哇咧!Service Facade 所 throw 的 business exception 頓時消失於無形,IDE/java compiler 不會告訴你沒 catch... 等到上線後才發覺就糗大啦。請務必拿掉這個 Exception,改成較小的 exception (最好完全不寫)。

  • Exception 不需多,各模組大概只需要一兩個即可,例如 AccountException, ATMException, ReceiptException... 如果有需要特別的 exception ,則繼承該模組最上層的 Exception。 例如 AccountException 是 account 模組的最大 exception,現有一特別的 exception 為 AccountNameAlreadyExistException 這個 AccountNameAlreadyExistException 就應該繼承 AccountException。

  • 我們 team 裡面用了一個特別的 excepton -- ResourseKeyBusinessException (繼承 BusinessException ) 這個 exception 的 message 便是 resource bundle 的 key:
    //AccountException 為 ResourceKeyBusinessException的 subclass
    
    public class AccountServiceImpl implement AccountService {
       public void createAccount(String name) throws AccountException {
            if(isNameAlreadyExist(name)) {
    
               // key 和 args[],定義在 MessageResource.properties
               throw new AccountException("account.name.duplicate", name) ;
            }
            //... do other logic...  
       }
    }
    
    在 Action 裡我們可以這樣寫:
    
       public ActionForward execute(ActionMapping mapping, ActionForm form,
                HttpServletRequest request, HttpServletResponse response) {
            String name = request.getParameter("name") ;
            try {
                getAccountService().createAccount(name) ;   
                // forward to success....
            } catch (AccountException e) {
                StrutsUtils.saveError(request, e) ;//自動轉換為 ActionMessage 
                //do some resource cleanup
                return mapping.getInputForward() ;
            }
       }
    StrutsUtils 是另外自己寫的小工具,它將 ResourceKeyBusinessException 中的 key 和 args 直接轉換為 ActionMessage,然後再呼叫 Action 的 saveErrors(...)。採用 ResourceKeyBusinessException 之後,就不必替各種小狀況單獨做一個 Exception 的 class 了 (比如說上面提到的 AccountNameAlreadyExistException ),只要一個 root exception:AccountException 即可。 也許會有人覺得這種做法將 struts 的東西帶到 business 層了,其實並不會... 試想如果你想將 exception 的訊息做成 i18n 的話,方法大概也是這樣做。況且 ResourceKeyBusinessException 並沒有 import 任何 struts 的 class 啊!


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>