19 July 2005

Let's continue discussing Spring/Struts integration:

public class CreateAccountAction extends ActionSupport {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {
        AccountForm aform = (AccountForm) form;
        User createdUser = 
              getAccountService().createAccount(aform.getName(), aform.getPassword());
        getMailService().notifyUser(createUser);
        request.getSession(false).setAttribute("user", created) ;
        return mapping.findForward("success");
    }

    private AccountService getAccountService() {//call getBean() from Spring } 
    private MailService getMailService() {      //call getBean() from Spring } 
    
}

上面的程式為 Action 的某一片段,內容是呼叫 accountService.createAccount() 建立帳號,再呼叫 mailService.notifyUser() 寄信給該新帳號的使用者。所有的 Business Logic 都寫在 service layer 裡,所以 Action 內很精簡。但是… 乍看之下是沒有啥問題,可是卻犯了一個嚴重的錯誤 -- Unit of Work。

如果沒有特別的狀況的話,通常 Action 都相對應使用者的 "一個決定"。對使用者來說這 "一個" 是不可分割的 (atomic)。因此一個 Action 只會有、也只能有一個 Transaction。上面 Action 範例中呼叫了兩次 service method,也就開啟了兩次 Transaction (提一下,在 Spring 裡,Transaction 的 boundary 是以 service facade 的 method 做為區間)。"一個動作" 包含了兩個 Transaction... 後果可想而知。

除了 break unit of work 之外,呼叫兩次 service method 還有其他 side effect:

  • 程式碼較難維護 -- 開發人員若將 Business Logic 都寫在 Service 裡,那麼後面的人接手維護時,他只要看 Service 就好了,不用去查東查西的。如果混了一些logic 在 Action ,後面的人怎麼知道?他還得一個一個去查,而且哪些 Action 用到哪些 Service 的 method 是很難查的。
  • StrutsTestCase 變的難寫 -- StrutsTestCase 本身就要處理 ActionForm, forward, validate 等等一堆鎖事,如果還要顧慮其他的 logic,花精力去做 fixture 什麼的,那是自討苦吃...
  • 程式較秏資源 -- Call 兩次/三次 service 就是開 transaction 兩次/三次... 開 database transaction 應該不便宜吧?
  • 造成 API unstable:這點就比較細了。如果遵守只在 Action 中 呼叫一個 service method,那麼為了回傳多個結果給 Action,我們勢必會將回傳變數設計成 Map,或是另外建立一個的專門回傳用 javaBean。今天如果使用者要求要在網頁上多 show 一個值出來,我們只要將 map 裡多加一個項目或者是替 java bean 多加一個 field 就搞定了,Action 的 code 與 Service 的 interface 都不用去動到。反之,如果用 Action call 兩次 service 的做法,那麼下次有新需求時,一不小心,就會讓 Action 去 call 第三次,第四次...,而且改了 service 之後,Action 跟著也要改,Action 就越來越大...... 維護性自然就差了。

改正後的寫法應為:

public class CreateAccountAction extends ActionSupport {
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {
        AccountForm aform = (AccountForm) form;

        //將 mailService inject 到 accountService 中,讓 createAccount 去呼叫
        User createdUser = 
              getAccountService().createAccount(aform.getName(), aform.getPassword());

        request.getSession(false).setAttribute("user", created) ;
        return mapping.findForward("success");
    }
    private AccountService getAccountService() {//call getBean() from Spring }    
}

這件事看起來很小很簡單,但是在我們的 Team 中卻常常看到這種錯誤。歸究原因,一方面可能是 Spring 將 Transaction 藏的太好了,所以 developer 常常會忘記。另一方面也可能是 developer 本身對 Unit of work 觀念不大好造成的。

ps. 基於同樣的理由,個人也不建議使用 Action Chaining (即數個 Action 連鎖來完成使用者的 "一個決定"),這也一樣會有 Transaction 被分割的問題。