l

2010年3月23日 星期二

敏捷式例外處理設計 (7):『終止』或『繼續』

3/23 21:38~23:34

『終止』或『繼續』,這是每個人遇到『困難』都必須要做出的決定:
  • 宅男:玩 game 卡關,要休息,還是要熬夜拼下去?
  • 情侶:對方劈腿,要分手,還是繼續交往?
  • 夫妻:個性不合,要離婚,還是繼續嘗試?
  • 觀眾:連續劇越演越扯,要轉台,還是繼續看下去?
  • 員工:公司太爛,要離職,還是繼續苦撐?
  • 工程師:期限到了,要無視於這麼多 bugs 把產品推出,還是要無限期 debug 下去?
  • 老闆:錢燒光了,要關門大吉,還是借錢繼續營運下去?
  • 學生:功課沒寫,要蹺課,還是勇敢的上學去?
當程式遇到『例外』的時候,programmers 同樣要做出『終止』或『繼續』的決定。

  • 終止:結束目前執行中的 method 或 function 並丟出例外讓 caller 處理。
  • 繼續:繼續目前執行中的 method 或 function 。對於執行過程中發生的例外,可能只 log 下來,或是收集起來用傳回值 (return value) 或是 collecting parameter 的方式回傳給 caller。

所以,設計一個程式的例外處理,除了要決定前幾篇文章中所提到的『強健度等級』以外,還要決定遇到例外的時候程式是要採取『終止』或『繼續』的策略。在 C++, Java, C# 這些語言,由於採用 termination model, 所以很自然的,當例外發生的時候,預設是採取『終止』策略 (丟出例外並且 pop current stack frame),所以很自然地 programmers 就預設接受這種方法。但有時候『終止』並不太合適,例如:

  • 執行批次工作:假設鄉民們寫一支程式,一次新增 100 個使用者。假設第 5 個使用者的資料有問題,在『終止』模式下,程式終止執行並丟出例外,後面沒有問題的使用者就沒有辦法繼續新增了(當然這和需求有關,有時候客戶可能要求一 有錯誤立刻終止不要繼續做下去,也可能要求做 rollback。)。
  • 執行週期性或重複性工作:假設有一個 thread 每隔 5 秒要檢查 CPU loading 並且將檢查結果記錄下來。如果某次檢查時因為不明原因導致例外發生,那麼如果採取『終止』模式而結束 thread 執行,那麼就違反了週期性執行的需求了。
雖然支援例外處理的程式語言 (C++, Java, C# 等等) 告訴廣大的群眾:『程式發生錯誤時要丟出例外來通知 caller』,但是如果你的程式採取的是『繼續執行』策略,此時就不應該丟出例外了。那要怎麼做?

執行批次工作

如果是執行批次工作,那麼可以用傳回值或是 collecting parameter 來將執行結果通知 caller。先看一個使用傳回值的例子:

public IErrorCollector doIt(List aList){

  IErrorCollector collector = new ErrorCollector();
     for (Job job : aList) {
         try {
       job.run(); 
       collector.addSuccess(job);
        }
        catch(Exception e)
        {
      collector.addError(e);
       }
  }
   return result;
}

如果程式本身會使用到傳回值,或是需要接受好幾個不同 methods 的執行錯誤結果,那麼就可以使用 collecting parameter 。


public int doIt(List aList, IErrorCollector aCollector){

  int result = 0;
 
     for (Job job : aList) {
         try {
       result += job.run(); 
       aCollector.addSuccess(job);
        }
        catch(Exception e)
        {
      aCollector.addError(e);
       }
  }

   return result;
}

或是


public int doIt(List aList, IErrorCollector aCollector){

  int result = 0;
 
     for (Job job : aList) {
       result += funA(job, aCollector);
       result += funB(job, aCollector); 
       result += funC(job, aCollector); 
     }

   return result;
}

上面程式片段中的 funA, funB, funC 分別對 job 做不同的處理,且保證不會丟出例外(因為使用了 big outer try block),並且將執行過程中發生的例外用 aCollector 收集起來最後回報給 caller 。


執行週期性或重複性工作

在執行週期性工作時如果遇到例外,最常見的方法就是把例外 log 下來。請看下面這個『意思到了但正確性可能不足』的例子:
 

public void run(){

  int result = 0;
 
     while (!Thread.currentThread().isInterrupted()) {
      try {       
         Task task = fQueue.take(); // blocking call
         task.execute();
      }
      catch(InterruptedException e){
         Thread.currentThread().interrupt();
      }
      catch(Exception e) {
         logger.error(e);
      }     
     } 
}

重點就是除非發生 InterruptedException,否則 run() 會一直執行。

結論

例外發生時決定『終止』或『繼續』將會影響例外處理的實做方法。例如,符合 G1 等級 (error-reporting) 的程式若是採用『終止』模式則必須丟出一個 (通常是 unchecked) exception。同樣是達到 G1 等級的程式,若是採用『繼續』模式,則依據執行的工作屬於『批次』或是『週期性/重複性』,而可以採用傳回值、collecting parameter、或是 log 來回報錯誤。

友藏內心獨白:繼續寫下去幾個月後就可以出書了...嘿嘿。不過,這種偏門的書有人看嗎?

    沒有留言:

    張貼留言