l

2010年3月26日 星期五

停掉生產線

3/26 21:39~23:05

『品質是內建,不是外加的』這句話應該算是常識了,現在大部分的人讀起來已經覺的沒什麼特別。但是在 19 年前,當 Teddy 還是個青澀少年時,在『由 C 到 C++: 物件導向革命』這本書中第一次看到這個觀念,卻是覺得很新奇:

哇,原來產品的品質在做好之後便已經決定了,更多的測試並不會增加產品的品質,所以要提昇產品的品質便需要提昇產品製作流程與流程中的每個活動的品質。

聽起來很有道理,但是,哪有可能!程式寫好不交給別人測試,能有這樣的勇氣直接拿給客戶用嗎?『暈倒死』的『藍色死亡畫面』看多了,電腦上面 reset 按鈕也按到手軟,再聽到『如何寫出零錯誤的程式』這類的話,心裡都會偷笑。久而久之,軟體有 bugs 算是正常,用到沒有 bugs 的軟體才是要懷疑是不是看到鬼。長此以往,programmers 對於 bugs 見怪不怪,因此對於如何提高軟體品質這檔子事的警覺性,也越來越低了。在業界中屢見不鮮的例子:

PM:這個功能做好了嗎?
Programmer:做好了,沒有問題。
PM:我剛剛測了一下,有很多 bugs...
Programmer:你咬我啊...

夜深人靜時,相信 programmers 的內心偶而也浮出小小的吶喊『我到底是在開發功能還是在開發 bugs?』。

軟體工程的主要目的之一,就是要探討如何做出高品質的軟體。達到這個目的有很多不同的手段,Teddy 在念研究所時,曾經修過 PSP (Personal Software Process) 這門課,PSP 希望 programmers 可以藉由多種不同層次的 review (design, code, etc)、紀錄一狗票的資料 (以分鐘為單位的 time log, 新增幾行程式碼, 修改幾行程式碼, 開發過程中所有發生的 bugs...) 、以及定期提出改善方案等手段來提昇品質。立意良好,但光是聽起來就覺的有點『違反人性』,窒礙難行。光是修課過程中寫 10 支小程式就快被搞死了,玩玩 Sony PSP (PlayStation Portable) 還差不多,怎麼可能用 PSP 來開發軟體...。

由於程式是人寫出來的 (程式產生器也是人寫的),所以各種軟體品質改善方法或是開發流程,無不強調如何減少人為錯誤。因為很多 programmers (至少在台灣是這樣吧) 已經養成『差不多先生』的心態,因此 Teddy 認為無論採用何種方法,首要之務便是以『符合人性的方式』提昇 programmers 的 羞恥心 警覺心,讓他們慢慢覺沒有 bugs (或是很低的 bug rate)是常態,而非不可能。要怎麼做?

Teddy 最近在讀 Lean 和 Toyota Production System (TPS) 的書,其中有一個作法叫做 Stop the Line--停掉生產線,Teddy 覺的滿有趣的,也很想來試試。Stop the Line 大意就是說:如果一發現不良品,就馬上停止整條生產線,一直到找到問題的根源 (root cause) 為止。乍看之下覺的日本人怎麼這麼笨,整條生產線停工,那成本有多高?但是仔細一想,如果不徹底找出產生不良品的原因,這些生產出來的不良品都是浪費。更糟的是,如果不良品流入市面,商譽的損失與後續維護成本更是高的嚇人。

也許敏捷團隊都應該安裝一個類似救護車或是消防車上面的『警示燈』,當一發現 bug 的時候就按下這個燈,啟動之後還會發出『偶一、偶一』的聲音。此時大家都放下手邊的工作,一起來徹底找出造成 bug 的根本原因,並討論如何避免下次再發生同樣的 bug。這樣做的好處至少有以下兩點:
  • 提高警覺:由於一發現 bug 需要暫停所有人的工作,所以大家都不會希望這個 bug 是自己造成的。因此在施工的時候,便會想辦法提昇軟體的品質。例如,主動找人幫你做 code review 、執行 pair programming、多做 unit testing、實施 test-driven development (TDD) 等等。甚至如果有人覺的 PSP 有幫助,也可以採用  PSP。總之,改善方法不限,由團隊自行決定。
  • 提高共識:所有人一起找出原因並且討論改善方案,因此對於如何發生錯誤以及如何避免再次發生都會有比較高的共識。
但是,如果你的團隊沒有專屬的辦公室而且工作環境中還有其他團隊成員,如果真的裝了一個警示燈,以一般軟體開發團隊發生  bugs 的頻率,可能會把其他人吵死吧...

友藏內心獨白:或許可以考慮每個人發一個像是在『伯朗咖啡』點餐時所拿到的『振動器』來取代警示燈。

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 來回報錯誤。

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

    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 介紹完畢。

    2010年3月19日 星期五

    萬事起頭難:如何開始第一個 Sprint?

    03/19 22:48~23:58

    幾個月前有人問 Teddy,要如何開始第一個 sprint?按照 Scrum 的講法,只要有 product backlog(就是說已經有可以開工的需求)就可以開始第一個 sprint。問題是,實際執行起來好像沒那麼簡單。想像一下,假設你的團隊要開發一個新的『網路性向測驗系統』,很幸運的,你的 Product Owner 已經把需求大致列出,因此你有了一個可以開始施工的 product backlog。但是這個系統還希望能作到下列幾項『非功能需求』:

    • 跨平台
    • 支援多種不同的資料庫,例如 MySQL,PostgresSQL,Oracle,MS SQL 等等。
    • UI 容易使用且必須支援目前主流的瀏覽器
    • 要具備擴充性(這一點很抽象,但是似乎每個軟體系統都想要有擴充性)
    你的團隊之前都在用微軟 .NET 技術開發,由於『跨平台』的需求,你正考慮是否改用 Java(PS:你知道 .NET 也有 Linux 版的 Mono 可以用... 但是怕相容性的問題所以暫時不考慮)。問題來了,你的團隊對於 Java 技術不熟,之前也沒有同時支援多種不同資料庫的經驗。對於 Web UI 要使用哪種方式 AJAX 元件也不了解。更慘的是,要如何設計一個具備『擴充性』的軟體架構實在是需要時間來規劃設計。

    那麼,就算基本的需求已經有了,技術上很多方向都不確定,如何直接開始第一個 sprint 呢?如果依照 Scrum 的說法,假設第一個 sprint 先挑三個 stories 來做,這三個 stories 會橫跨系統的每個 layers (end-to-end stories)。所以,系統的架構與所要使用的技術就會這樣慢慢地,慢慢地『長出來』。講是這樣講沒錯,可是,團隊成員根本連要使用哪種 Java 技術架構來開發都不知道,真不知道要如何一下子就立刻實做功能。也許就是為了解決專案起頭的難題,所以最早 Scrum 的流程其實是長這樣子滴:


    注意到了嗎? Sprints 開始之前有一個 Planning & System Architecture 階段。可是後來 Scrum 的流程圖卻都演變成下面這個樣子:


    Planning & System Architecture 階段到底被誰吃掉了,難道真的有那麼順利可以直接就進入第一個 Sprint?

    ************

    這個疑問應該是很多想執行 Scrum 的鄉民們都會遇到的問題。以下 Teddy 列出幾種可能性給鄉民們『僅供參考』一下:


    1. 根本不會有這個問題:你很好狗運,新的案子使用的技術你的團隊已經很熟悉了。所以,沒問題,只要有了 product backlog 就可以開工大吉了。
    2. 帶著鋼盔向前衝:就給它完全相信 Scrum 的精神,直接從完成 stories 的過程中逐步地來釐清所要選用的技術與軟體架構。可能遇到的風險是,在開發的過程中,所選用的技術或是軟體架構改了好幾次。
    3. 先做 technical stories:在 Scrum 中將技術性的問題(例如,架設 continuous integration system)列成 technical stories 是常見的一種作法。所以,第一個 sprint 所選擇 stories 可能都是所謂的 technical stories,包括訂定軟體架構雛型,決定 UI 技術,決定連接資料庫技術等 。不過,這麼做好像跟第一張圖裡面的 Planning & System Architecture 階段所要做的事好像喔。
    4. 規劃一個 sprint 0:這個作法在 Becoming Agile: in an imperfect world 這本書裡面有提到,在書中稱為『iteration 0』(不過這本書被 Bas 罵到臭頭...請看 Amazon 上面 Bas 對於這本書的評論)。這個 sprint 0 要做的事說穿了就是原本的 Planning & System Architecture 階段,只是名稱不一樣而已。
    『正港 Scrum』可能不會建議鄉民們採用 4 甚至是 3,因為這樣看起來就有點像是 big up-front design 的嫌疑,然後一不小心就退化成傳統 waterfall 流程了。但是,如果你的團隊就是需要一個小小的『sprint 0』來作為開端,說真的 Teddy 是覺的這樣一點點的事前準備應該是 OK 的啦。畢竟 agile 並不是反對 up-front design,而是反對 big up-front design。但是要記得,sprint 0 之後就要開始 sprint 1 真正開發功能了,不能變成 sprint 0.0 -->sprint 0.1--> sprint 0.2-->sprint 0.3--> sprint 0.x 這樣就陷入了 big up-front design 了。

    友藏內心獨白:叔叔有練過,小朋友如果要學的話要小心,逆練『九陰真經』如果走火入魔後果請自行負責。

    2010年3月18日 星期四

    還少一本書:Smalltalk Best Practice Patterns

    03/18 22:49~23:55

    連續寫了幾天的 exception handling,今天換個口味,介紹一本在台灣應該算是比較冷門的書:Smalltalk Best Practice Patterns, by Kent Beck.

    雖然 Kent Beck 是 Teddy 的偶像,但是 Teddy 也不是『好野』到可以買下大師所寫得每一本書。當初看到書名想說這是一本談 Smalltalk 的書,而 Teddy 又完全不懂 Smalltalk,所以雖然書名也有 Patterns 這個字,但是也沒特別想買(在台灣好像找不到進口這本書的書商,當初是直接從 Amazon 買的)。那,為什麼最後還是買了,而且還要推薦?

    記得當年 Teddy 在學 JUnit 設計的時候,看到 JUnit 使用到 『Collecting Parameter』這個以前沒有看過的 pattern (大膽,居然使用了 GoF 之外的 pattern....),而這個 pattern 正是 Kent Beck 在這本書中有介紹到的。此時 Teddy 猜想,這本書應該還有一些其他好料的,所以雖然看不懂 Smalltalk 還是買下來了。

    先自首一下,這本書從 2002年5月17號拿到手,到現在 Teddy 只看了 1/3。不過這不是重點,重點是,2003 年 Teddy 在學 Eclipse 設計的時候,居然發現 Eclipse 還有 SWT 也用到好幾個在這本書中有提到的 patterns。由於買了這本書,所以就比較容易了解 Eclipse, SWT 裡面一些 patterns 的用法 (廣義的來說,應該是 Eclipse, SWT 受到了 Smalltalk 的若干影響...)。

    舉一個例子,SWT 的 UI 實做了『Variable State』這個 pattern, Teddy 把書中關於這個 pattern 的說明引用一下:

    Problem: How do you represent state whose presence varies from instance to instance?

    Solution: Put variables that only some instances will have in a Dictionary stored in an instance variable called "properties." Implement "propertyAt: aSymbol" and "propertiesAt: aSymbal put: anObject" to access properties.

    翻成白話文就是說,如果屬於同一個 class 的不同 instances 需要各自保有不同的狀態, 那麼請在該 class 中宣告一個 Dictionary (在 Java 裡面可以用 HashTable 之類的物件) 的 data member (attribute)。每一個由這個 class 產生的不同 instances 就可以用這個 Dictionary 來存儲各自所需的資料(鄉民甲:這只是從英文的文言文,換成中文的文言文啊)。

    不知道鄉民們是否有這樣的經驗,有一個現有的 class 雖然符合你的需要,但是你還需要增加一,兩個 data member 來儲存其他狀態。如果這個現有的 class 沒有實做 variable state pattern,那麼你可能就必須要利用『繼承』來將你所要增加的 data member 寫在 subclass 裡面。鄉民們應該也都知道好的物件導向設計其實對於『繼承』的使用是很『節制』的,在上述例子中算是有點濫用繼承。舉的 SWT 的例子,在寫 GUI 程式的時候,有時候為了方便起見會把一些變數(或是物件)直接存在 UI 元件上面。假設你用了一個 Button 元件,因為否種原因你想把某個變數存在這個 Button 上面(至於是什麼原因,Teddy 也不知道..!$!@#%)。如果這個 Button 元件沒有實做 variable state pattern,那麼你就要寫一個 MyButton 的 subclass 來儲存這個變數,有點殺雞用火箭炮的感覺。

    如果 Teddy 上面所講的鄉民們還是看不懂,沒關係。買一送一,再舉一個 .NET 的例子。有一次 Teddy 在研究 exception handling 的時候意外發現 .NET 的 Exception 類別好像是從 .NET 2.0 還是 3.0 之後,就實做了 variable state  這個 pattern。很可惜 Java 的 Exception 並沒有實做這個 pattern,所以假設你想要在 Java 的 IOException 上面多夾帶一些資料,以便於讓收到 exception 的人可以做進階的處理,很抱歉,你必須繼承 IOException 才行。

    **************

    好像有點離題,變成在介紹 variable state pattern。總之,這本書中還有很多在其他地方比較看不到的資料,例如,如何幫 instance variable (i.e., data member) 取名字和 pluggable selector 等等, 值得買來珍藏。

    最後,引用書中一小段 Teddy 很喜歡的文字作為結尾。


    To me, development consists of two processes that feed each other. First, you figure out what you want the computer to do. Then, you instruct the computer to do it. Trying to write those instructions inevitably changes what you want the computer to do, and so it goes. (PS:這一段是起頭,後面那一段才是重點)


    In this model, coding isn't the poor handmaiden of design or analysis. Coding is where your fuzzy, comfortable ideas awaken in the harsh dawn of reality. It is where you learn what your computer can do. If you stop coding, you stop learning.

    友藏內心獨白: 據說 "If you stop coding, you stop learning." 是 Teddy 的座右銘。

    2010年3月17日 星期三

    敏捷式例外處理設計 (5):我到底哪裡做錯之 spare handler

    03/17 20:25~22:13


    以下的內容需要先閱讀『敏捷式例外處理設計的第一步:決定例外處理等級』比較容易理解。

    今天 Teddy 要談 spare handler 這個 exception handling bad smellSpare handler 的意思是把 exception handler (就是寫在 catch clause 裡面的程式碼) 當成是其相對應 try clause 的備胎(有點繞口,多念幾次)。也就是說當寫在 try clause 裡面的程式碼發生例外的時候,exception handler 會嘗試提供另外一種實做來達成原本在 try clause 裡面所要達成的任務。請看以下 Java 程式片段:

    public void readUser(){

        try {
              // primary implementation
        }
        catch (SomeException e){
            try {
                  // alternative implementation
            }
            catch (AnotherException e){
                  throw new FailureException(e);
           }
       }
    }

    因為寫在 catch clause 裡面的『備胎程式』在執行的時候也可能會發生例外,因此 spare handler 就很容易形成一個 nested try block(昨天介紹過的 smell)。此外,由於 spare handler 的效果相當於透過『重試』(retry) 來達成原本 try clause 所要提供的服務(PS:重試可以是重複執行原本一模一樣的程式,或是執行另外一種實做的程式,或是提供不同的參數),從上面的例子來看這樣的程式結構只能達到『重試一次』的目的,如果要達到『重試 N 次』勢必會造成更深的 nested try block


    *******

    Introduce resourceful try clause

    要移除 spare handler 可以套用 Introduce resourceful try clause 這個 refactoring,請看:


    public void readUser(){

        try {
              // primary implementation
        }
         catch (SomeException e){
                try {
                     // alternative implementation
                }
                catch (AnotherException e){
                      throw new FailureException(e);
               }
         }
    }

    變成 

    public void readUser(){

        int attempt = 0;
        int maxAttempt = 2;
        boolean retry = false;

        do {
              try {
                    retry = false;
                    if (attempt == 0)
                             // primary implementation
                   else
                            // alternative implementation
              }
              catch (SomeException e){
                   attempt++;
                   retry = true;
                  if (attempt > maxAttempt)
                           throw new FailureException(e);
            }
        } while(attempt <= maxAttempt && retry)
    }

    Resourceful try clause』的意思是說把 try clause 寫成具備『多才多藝』(擁有一種以上的方法可以提供相同的服務)的程式碼,如此一來當例外發生的時候try clause 就可以依據 programmers 事先設定的規則來決定用何種方法提供服務。由於 Java 的例外處裡模式 (exception handling model)是採用 termination model,也就是說當例外發生的時候會終止目前控制流程,因此當例外發生之後為了讓 try clause 有機會繼續執行,必須將 try block 寫在 loop 裡面(在上面的例子中使用了一個 do-while loop),然後宣告必要的變數來控制重試的次數。

    上面的例子算是『呷粗飽』的方法,程式看起來有點亂,感覺一不小心控制邏輯就會寫錯。如果要『吃的精緻一點』,則可以寫一個 class 來包裝控制重試次數的變數與邏輯。如果要求要吃到『三井宴』的等級,那麼另外一種更高檔的方法則可以將整個 do-while loop 還有控制重試邏輯的程式寫成一個 RepeatExecutor class, 接著將 try clause 所以提供的各種不同實做方法 (implementation methods) 當成參數傳給這個 RepeatExecutor class。然後 Retry class 可以透過 reflection 的方式來執行不同的實做方法。看一個簡單的例子:

    public class Acceptor{

           private ServerSocket fSocket = null;
          
          public void createSocket(Integer port) throws IOException {
                  fSocket = new ServerSocket(port);
         }
    }

    public void create Acceptor(Integer port) thorws
                                                 CannotCreateAcceptorException {

        Acceptor acceptor = new Acceptor();
        IRepeatExecutor repair = new RepearExecutor();
        repair.setAttempt(2);
        repair.setMode(IRepeatExecutor.Mode.defaultValue);
        repair.addDefaultArgs(port + 1);
        repair.addDefaultArgs(port + 2);

        try {
             repair.run(acceptor, “createSocket”, port);
        }
        catch(RepeatExecutorFailureException e){
             throw new CannotCreateAcceptorExecution(e);
        }
    }

    上面的例子,try clause 裡面剩下一行程式:

         repair.run(acceptor, “createSocket”, port);

    repair 物件透過 reflection port 當成參數執行 acceptor 物件的 createSocket() method。如果執行結果發生例外,則 repair 會重試兩次:

         repair.setAttempt(2);

    第一次重試會將 port + 1 當成參數傳給 createSocket():

         repair.addDefaultArgs(port + 1);

    第二次重試會將 port + 2 當成參數傳給 createSocket():
         repair.addDefaultArgs(port + 2);

    repair 物件支援下列幾種常見的重試方法,分別是:
    • 用原來的參數,重複執行原本的方法 (primary implementation)
    • 用新的參數,重複執行原本的方法
    • 用原來的參數,執行其他方法 (alternative implementation)
    • 用新的參數,執行其他方法 (alternative implementation)

    上面這個例子採用的是第二種:用新的參數,重複執行原本的方法。無論採用哪種方法,重點是try clause變成了 resourceful try clause。而這是一種讓程式達到 G3 (behavior-recovery) 的常用方法(前提是當例外發生之後要確保程式狀態是對的)。

    友藏內心獨白:一下子從 G1跳到 G3,好像有點複雜...達到 G3 果然比較『厚工』。

    2010年3月16日 星期二

    敏捷式例外處理設計 (4):我到底哪裡做錯之 nested try block

    03/16 20:4421:55

    以下的內容需要先閱讀『敏捷式例外處理設計的第一步:決定例外處理等級』比較容易理解。

    今天 Teddy 要談 nested try block 這個 exception handling bad smell。請看以下 Java 程式片段:

    public void doIt(){

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



    請注意finally clause 裡面出現了另外一個 try block,這個 try block 被另外一個 try block 包圍,因此裡面的這個 try block 就稱為 nested try block。那麼 nested try block 有什麼問題,為什麼是一個 bad smell


    廣義的來說,程式碼出現 nested code constructs (例如很多層的 if-then-else, for while loops) 將會使程式變得不容易閱讀,測試,維護。這個問題同樣也會出現在 nested try block 之中。請鄉民們再看一下上面那一小段 code,才兩層的 nested try block 就已經有點讓人眼花撩亂了。


    第二個問題,也是比較容易被忽略的問題,就是nested try block可能會造成 duplicate code。寫在 finally clause 裡面的這一段 code :
     
    try {
          if (in != null)
               in.close();
    }
    catch(IOException e)
    {
           // log the exception
    }


    是一段常見用來釋放 IO 資源的程式碼(cleanup code),先判斷 IO 物件是否有被正常產生(不是 null)。如果是,就呼叫 close() 來歸還 IO 資源(通常是還給作業系統)。這樣的程式碼其實是可以重複使用的,如果直接在 finally clause 裡面用一個 nested try block cleanup code 包起來,就造成 duplicate code。這樣的 duplicate code 其實還滿容易看到的,但卻容易被忽略。


    *******

    Replace nested try block with method


    要移除 nested try block 可以套用 Replace Nested try Block with Method 這個 refactoring,請看:



    public void doIt(){

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

    變成



    public void doIt(){

        FileInputStream in = null;
        try {
              in = new FileInputStream(...);
        }
         catch (IOException e){
             // do something
        }
        finally {
            closeIO(in);
        }
    private void closeIO(Closeable c){

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

    如果要共用 closeIO() 這個 method,可以把它變成 public static 然後寫在某個 utility class 上面。基本上這個 refactoirng extract method 的概念是一樣的,只不過 replace nested try block with method 強調的是用來改善例外處裡的程式。

    另外,眼尖的鄉民們可能會注意到,在 closeIO() 裡面發生的例外被忽略了,這樣不是造成了前天所講的 dummy handler 這個 bad smell?光是從『程式碼結構』(code structure)來看,這的確是一個 dummy handler。不過由於這個 closeIO() 是放在 finally clause 裡面,此時並不適合套用 Replace Dummy Handler with Rethrow。因為如果在 finally clause 中丟出例外,則該例外將會『覆蓋』之前發生的例外(如果在 try clause 或是 catch clause 中有發生例外,那麼將會被 finally clause 所丟出的例外給覆蓋)。 所以,Java語言建議 programmers 不要在 finally clause 中丟出例外。因此,在這個限制之下,我們認為將例外記錄下來是一種妥善的例外處裡方法,也就不算是 dummy handler了。

    如果 closeIO() 真的發生例外那該怎麼辦? closeIO() 的例外代表 cleanup 發生問題,也就是程式(在這個例子裡面就是 Java 提供的 FileInputStream 這個物件可能有 bug)可能存在因為資源釋放失敗而導致的 memory leak。由於在此僅僅將例外記錄下來,因此 runtime 的時候程式還是會繼續執行,只能靠事後檢查 log file 來查看是否有問題。不過『理論上』,這種例外『應該』不會發生才對...

    友藏內心獨白:Java 的設計者有沒有想過IO 物件的 close() method 所丟出的 IOException 到底要如何處理?