星期六, 7月 09, 2005

Blog 搬家囉,這個地方不會再使用了.....


/**
* @deprecated 本 blog 已經停用請改用下面的網址
*
* @link 新Blog:JavaWorld jroller
*/
public void viewXexexBlog() ;

星期三, 7月 06, 2005

XP programming !

我們新專案已完成 phase I 了,爽!

buildhistory

這 張圖是我們 (六個人) 三個月來 pair 的成果,Y軸是每天 continuous integration 的 test case 的數量 (yes, 每天都是 success build)。四月初到四月中旬都是在分析需求,所以沒有增加 testcase。四月中開始則每天以20到30個 test case 不等,持續穩定的增加。最後七月總共1200 個 unit test。空白的部份則是週休二日。

雖然說穩定增加,其實裡面有兩週 (五月初和六月初)線是水平的。五月初那次是寫到400個test case 後發現架構有問題,所以整週都在做 refactoring (事後證明 refactoring 是對的) 六月初那次是大家去三天員工旅遊的啦~~~ 所以線也是平的 :-)

專案的平台是 Struts + Hibernate + Spring,除了JSP 之外,全部都寫了 Unit case。不知這樣的架構三個月 1200 個 test case 會不會算太少太慢?網路上從來沒看過相關文獻說....

我 們第一個專案 是 Struts + Hibernate平台,共 1500 個 Test Case,當時 Struts 並沒有寫 Unit Test,所以大多數是 business, database 相關的 test case。你猜總共要跑多久?總共要 40 分鐘!寫到後來每次都要等很久..... 沒力啊.... 而這新的專案呢? 1200 個 只需要 4 分鐘 (包含建立資料庫) ,非常非常地快,開發的節奏完全不一樣了,讚!這都歸功於 Spring, Easymock, 與新版 Ant 的功能,強力推薦大家使用!

pair 的成員上次提過了,三個男 + 三個女的 (我們這男女開發員比例約為 1:1)


第一組:
(主導性高) 重度感染 Test 的 男 developer (症狀是每天逼大家寫 Test)
(主導性低) 中間程度之男Developer (但不熟. Hibernate /Unit test ... )

第二組:
(主導性高) 正值懷孕的中上程度女 developer (我沒寫錯,你也沒看錯,是 "懷孕")
(主導性低) 中間程度之女 Developer (配合度高)

第三組:
(主導性中) 男新人,不過底子夠,自學性強
(主導性中) 女 java 新手,背景是 procedure, database SQL 出身,不過她是整個 team 裡最熟 domain 的。

第一組曾發生過內亂,"Test 男" 嫌 "不熟男" 程度跟不上。氣的想自己寫算了。後來 "不熟男"發奮圖強,所以暫時先合解。"Test 男" 也改變了做法,pair 的時候都讓 "不熟男" 操作鍵盤,好好訓練。現在 pair 經過三個月後,"不熟男" 漸漸有點成績出來了。雖說獨當一面還不大夠,但是已經可以獨立維護程式了,終於變 "熟男" 了。

第二組在技術和經驗都沒什麼問題啦,她們後來也寫了最多的 test case。比較大的問題是 "懷孕女" 正值懷孕期間.... 還好跟她搭當的是我們 team 中配合度極佳的 女 developer,所以一切相安無事。不過 "配合女" 由於鍵盤都被主導性極強的 "懷孕女"搶走,所以對 pair programming 不是有好感,還是希望能自己寫,有自己的發揮空間。

第三組是個人最擔心的一 組... 因為兩個人都是第一次在這裡開發 java 專案,前兩組的都有過舊專案的 "教訓",所以都會採取比較正確的寫法。三個月下來,雖然 code 有一些暇疵,不過還是漂亮的完成了任務。"底子夠男" 脾氣比 "Test 男" 好多了,跟新手 pair 比較有耐心。"女java新手" 除了 web 那些有的沒的學不大起來之外,其他的邊學邊寫之下也頗成氣候的。而且她對 domain 熟,抓的方向會比較準。有趣的是 "女 java 新手" 開發完後的結論是:"還是 procedure 好寫......"

如果這次不是這樣 pair,比方說改成 "Test 男" + "女java新手",也許 "女java新手" 會比較了解為何 java 要這麼複雜,為何所有程式都要寫 test。又如將 "不熟男" + "配合女" 一起 pair,那自然就不會有衝突,"配合女" 也拿的到鍵盤,比較有機會發揮了。再將 "懷孕女" + "底子夠男" pair,那麼這一組的程式就不會有暇疵,"底子夠男" 可直接吸收 "懷孕女" 的經驗,而 "底子夠男" 脾氣較好,比較不會影響 "懷孕女" 的胎氣...

當然這樣想是事後諸葛啦,誰知道這樣的 pair 會不會有新的問題咧?

(上面那一段用的詞句有點奇怪,希望大家看的懂啦...)


Anyway, 個人對這次 pair 的感受是 --

(1) pair programming 是最好的教育訓練的工具。原本高中低手都有,三個月後大家的水準馬上都拉上來了。未來我想 pair 的配對選擇方式應該會以教育訓練為第一個考量。

(2) pair programming 減輕很多壓力!不論是設計或是撰寫,你都有 partner 可以商量,分擔責任,我們 team 裡沒有專職的 SA/SD,所以 pair design 適時的發揮效用。另外,想請假也有 "完整" 代理人。而且你不用花時間維護太多的文件,你隨時都有接班人,文件自然能寫精簡一點啦

(3) 不用說,code quality 自然是標準水平以上了。如果這個專案是六個人分開寫,我真的不知道會變成什麼樣子... 就算三個月生東西出來了,大概也不能用吧....

總結。pair programming 不可能風平浪靜.... 有很多問題一一待克服.... 我們這先天的優點是男女比例一樣,我想如果是清一色的男性,那困難會大上很多。三個月下來,我相信我們 team 已經跨過 XP 的門檻了,相信未來的專案會繼續採用 XP -- 因為我們已經嘗到甜頭啦!

星期日, 5月 15, 2005

use AOP to simplify hibernateTemplate (Hibernate3)

Spring ORM 替 Hibernate 的 API (Session and Query) 做了一番修整,也就是 HibernateTemplate ,它最主要的目的是 resource 控管及 exception 的轉換。有了這個 persistence layer 的 code 就可以大量的減少並且降低出錯的機會。然而這個 API 卻抹剎了 Hibernate API 的簡單易用優點,舉例來說,如果藉由 hibernateTemplate 控制 Hibernate Session (ex. Criteria),唯一的做法就是寫很難看的 Callback:

public MyData findByNo(final String no) {
final String hql = " select data from MyData data "
+ " where data.no = :no ";

HibernateCallback hc = new HibernateCallback() {
public Object doInHibernate(Session session) {
Query q = session.createQuery(hql);
q.setString("no", no);
return q.uniqueResult();
}
};

return (MyData) hibernateTemplate.execute(hc);
}

inner class + final.... 有夠難看。下面要介紹的是,利用 Spring AOP 的 AutoProxy 的功能,自動將 DAO 的所有 method 用 HibernateCallback 包起來。如此一來,便可以自由在 DAO 中自由存取 Hibernate Session 了:

總共需要三個 class:
第一個是 Advice,也就是將所有 method 都先用 HibernateCallback 包起來,然後將 Hibernate Session 放入 HibernateDAOWarpper 裡。

public class HibernateDAOWrapperMethodInterceptor
implements MethodInterceptor {

private HibernateTemplate hibernateTemplate;

public Object invoke(final MethodInvocation methodInvocation)
throws Throwable {

try {
return hibernateTemplate.execute(new HibernateCallback() {

public Object doInHibernate(Session session)
throws HibernateException, SQLException {
HibernateDAOWrapper.currentSession.set(session);
try {
return methodInvocation.proceed();
} catch (Throwable e) {
if (e instanceof HibernateException) {
throw (HibernateException) e;
} else if (e instanceof SQLException) {
throw (SQLException) e;
} else {
//must re-throw as RuntimeException, and let
//hibernateTemplate deal with resource management
throw new ExceptionCarrier(e);
}
}
}

});
} catch (ExceptionCarrier e) {
throw e.throwable;
}

}

private static class ExceptionCarrier extends RuntimeException {
private Throwable throwable;

public ExceptionCarrier(Throwable t) {
this.throwable = t;
}
}

public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
}
接下來就是 AutoProxyCreator,當它遇到 bean 的 class 為 HibernateDAOWrapper 的 subclass 時,它就會自動替該 bean 做 proxy。

public class HibernateDAOWrapperAutoProxyCreator extends
AbstractAutoProxyCreator {

protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass,
String beanName, TargetSource customTargetSource)
throws BeansException {
if (HibernateDAOWrapper.class.isAssignableFrom(beanClass)) {
return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS;
} else {
return DO_NOT_PROXY;
}
}
}

有了這個,再搭配上面的 Advice,我們就可以將所有 HibernateDAOWrapper 的 subclass 的所有 method 都用 HibernateCallback 包起來。Spring 的設定如下:

<bean id="hibernateDAOWrapperAutoProxyCreator"
class="org.bioinfo.util.spring.dao.HibernateDAOWrapperAutoProxyCreator">
<property name="interceptorNames">
<list>
<value>hibernateDAOWrapperMethodInterceptor</value>
</list>
</property>
<!-- for use CGlib to create proxy,
(default to false, which will use Proxy if it detects interface presence) -->
<property name="proxyTargetClass">
<value>true</value>
</property>
</bean>
<bean id="hibernateDAOWrapperMethodInterceptor"
class="org.bioinfo.util.spring.dao.HibernateDAOWrapperMethodInterceptor">
<property name="hibernateTemplate">
<ref local="hibernateTemplate"/>
</property>
</bean>

這個只要設一次就夠了。接下來我們再來看看主角:

/**
* A special HibernateTemplate wrapper for DAO. You need to do:
*
* (1) Extends this class and use getSession() to obtain Hibernate3 Session
*
* <code>
* public class YourDAO extends HibernateDAOWrapper {
*
* public MyProject findByProjectCode(String code) {
* String hql = "select prj from MyProject prj where prj.code = :code";
*
* return (MyProject) getSession().createQuery(hql)
* .setString("code",code").uniqueResult();
* }
* }
* </code>
*
* (2) At your spring *.xml, just write one line:
*
* <code>
* <bean id="yourDAO" class="some.thing.YourDAO"></bean>
* </code>
*
* done !!
* @author ingram
*
*/
public abstract class HibernateDAOWrapper {

final static ThreadLocal currentSession = new ThreadLocal();

protected final Session getSession() {
return (Session) currentSession.get();
}
}

呵,小的可憐,它的功能就是提供 getSession() 的 method 給 subclass 的 DAO 而已。這個 session 來自於 Advice,並且放在 ThreadLocal裡,因此是 thread-safe 的。使用方式就如上面的 javadoc 所說,將 DAO 直接 subclass 這個 Wrapper,並在 spring 裡定義即可。

這樣一來 DAO 就可以快快樂樂的用 Hibernate API 啦,爽!

(註:僅適用 Hibernate3,於 Hibernate 3.0.3 + Spring 1.2 final 測試 )

星期六, 5月 14, 2005

(draft v2) Best Practice for Struts+Hibernate+Spring

最近又看了一本 Pro Spring,喔~ 裡面的第十一章 (Designing and implementating spring-based applications) 真是太棒囉!很多建議都相當的實用,解決了許多困惑我很久的疑問。這樣子差不多可以整理出一個通用的開發架構了:

o J2EE without EJB
採用 Struts + Hibernate +Spring 三個 framework 開發專案,每個framework都提供較 j2ee 更簡單、更快速、更open的實作。
  • Struts 提供 presentation layer 的各項服務
  • Hibernate 提供 domain layer 的 persistence 及 query 服務
  • Spring 提供 transaction/mail/remoting... 等等 middle-ware 的服務
o Writing unit test for everything
Struts Action/ActionForm, Hibernate DAO, Spring Service.... etc 從上到下所有的 java code 都依循 TDD 的方式開發。目前除了 jsp 之外,全部都要寫 unit test。

o 4 layers
  • Presentation Layer - 與使用者互動
  • Application Service Layer - 此層可明確表示 use case / user story / user requirement
  • Domain Layer - domain problem mapping as domain objects interaction.
  • Persistence Layer - 負責物件的儲存及查詢. (物件的中間生命週期)
o 保持 domain layer 乾淨
  • no DAO
    • 進入 domain layer 前,先將資料轉換為 domain object,無論是從 database 來或是使用者輸入。
    • domain object 間的 association,如果需要從 database query,盡可能改用 ORM 直接做associate ,而不是再透過 DAO 才連結。(除非 performance 的考量)
  • no Service
    • domain object 不應與任何 framework, container, context, enviroment... etc 等相關。所有存在於Domain Layer 的物件僅僅是互相連結的 POJO。
o Domain Object >= Persistence Object
  • Domain Object is not nessasary identical with Persistence Object(PO). just choose domain objects that need to persistence and query as PO.
  • All Persistence Object's getter/setter should be in default package scope.
o Design Domain Object fine grain and immutable
  • Do not afraid introduceing new Class.
  • Prefer immutable object. Immutable Object enforce you mapping domain problem to domain object correctly, and reduce maintain effort. Only Persistence Object has mutable characteristic.
  • no getter/setter. Object's property is hidden and only expose behavior to public.
  • no Singleton and no static. If you find something can apply singleton pattern, or design as static. It may be not belong to domain layer.
o Domain Object 跨全域
  • Domain Object 會傳到 presentation layer 與使用者互動,會傳到 application layer 使用 middle-ware 服務、會扮演 PO 進入 persistence layer 儲存。
  • 安全地跨全域的關鍵在於 Immutable。
  • DTO is non-sense.
o Classify Root of Domain Object in domain layer
  • ex. A Car has engine/wheel... etc. A engine can not work or sell directly. so typically Car class is Root of car/engine/wheel.
  • ex. An engine Factory produce various engines and sell them to down-stream factory. Obviously now the engine is Root class for this domain.
o Service Layer:
  • thin business logic
  • coordinate:
    • Root of Domain Object
    • DAO
    • Other middle-ware service (mail/remoting/transaction)
  • each method is unit of work of user's single operation
  • method boundary is transaction boundary, declare in Spring.
  • Spring managed resources are all in this layer.
  • Apply IoC in this layer heavily to make test easier. (flexible design)
  • Every service is a interface and pair with at least one implementation: FooService/FooServiceImpl
o Persistence Layer:
  • Not all database table has it's own DAO, only Root of Domain Object has DAO
  • See Hibernate 3 Reference chap. 7 to choose best association for your ORM
  • Efficiently use Hibernate's optimistic lock feature for concurrecy issue.
  • Efficiently use Hibernate's lazy/cache/fetch join technique to improve ORM performance
  • All POs have an primary key: 'Long id'. DO NOT use compose key !
  • All POs' getter/setter scope is package default (private is better)
  • Perfer HibernateTemplate and JdbcTemplate, with this we don't require additional interface for DAOs.
o Struts ActionForm:
  • ActionForm will be populated from Domain Object for user input.
  • After user input, the ActionForm should convert user's input back to domain object if possible.
  • It's ok that treat ActionForm as Domain Object and transfer into service layer, but use with care because ActionForm is highly mutable. If you want to protect Domain Layer and reuse ActionForm's code, Just let ActionForm implement Domain Object interface.
o Struts Action:
  • each Action only call one method of a single service. (because of transaction boundary)
  • Action's responsibility is to coordinate Invoking service method, ActionForm population, ActionForward flow, and compose ActionMessage... etc, nothing more.
  • does nothing about business logic.
o Design Business Exception:
  • All business exceptions are checked and user recoverable. if the exception is user un-recoverable, it should be classified as RuntimeException
  • All business exception should extends 'BusinessException', the Root of all business exceptions
  • If you want your business exception support i18n, use 'ResourceKeyBusinessException', which extends from 'BusinessException'
  • Each module only has one its own Root business exception, and all rest of exceptions for this module should extends its own Root business exception
  • This is checked exception, but you will not try to catch it except in Struts Action. business exception is 'user' recoverable, so it should always throw away until Struts Action. Struts Action catch Business Exception, convert to meaningful message and forward user to error page.
好像越寫越多... 看起來亂亂的... 而且英文佔了一大半 -_-;
啊,就先這樣就好了,以後有空在補。

星期六, 4月 30, 2005

反微軟終結

這個站是我見過最久的網站... (BBS 不算) 終於停了...
哎... 微軟什麼時候才會倒...

星期日, 4月 24, 2005

Pair Programming

去年以來個人一直在 team 裡疾呼 unit-test 的重要。現在整個 team 裡除了幾個不曾跟 heavy-tester 一起合作的人之外,幾乎都了解 test 的好處和重要 (有些人是沒寫test 吃了大虧,有些人先是被逼著寫,後來嘗到了甜頭 )。去年的開發幾乎完全都是個人單打獨鬥,然後單純互相討論,今年這個新專案由六個人組成,好玩的是分工完之後就自動兩兩成對 pair 了。記得去年前 pair 時大家都哇哇叫,今年好像因為個別的不同理由不得不 pair 啊,目前 pair 的分配是:
  • pair A -- 資深(主導性強) + test 新手
  • pair B -- 資深(主導性強) + 中等
  • pair C -- java 新手 + test 新手 (兩人主導性差不多)
上面的資深資淺以 test 的資歷來計,我個人認為 unit-test 的 quality 很適合用來判斷一個 developer 的生產力。

A組沒有 pair 跟本做不下去,完全由資深帶領資淺,以免出差錯,不過資深的步調極快,資淺的步調極慢.........

C組的只能 pair 了,因為其中一人剛學 java.... 另一人功力夠,不過還沒實際作過專案,也沒有 test 的經驗

B組的都有 test 經驗,而且都能夠獨力作業,其實不 pair 好像也沒差,不過看起來 pair 的理由是有人可以互相討論,減輕壓力。

這是現在看到的現象,不知再過三個月會變成如何呢?呵呵,接下來討論一下經驗和構想吧:
  • Personality
我發現讓兩個主導性強的或是兩個主導性弱的 pair 在一起問題會很多。兩個主導性強的會堅持己見,很容易吵在一起,不僅氣氛差,進度也會變慢。兩個弱的則是遇到問題會卡很久,然後慢吞吞的,不曉得東西什麼時候會出來,很危險。
  • Relax Time Control
這個..... 我們目前完全沒有控制作息的時間,目前我的構想是 break 個兩次。讓兩人休息休息,下個禮拜建議大家實施看看好了。
  • Standup Meeting
XP 的早上一開始都會開個幾分鐘的小會議,目前我們也還沒有試過,不過這個大概很難吧... 台灣人好像很不喜歡在制式的會議上發言.... 大家覺得能躲就躲.... 如果這個要推行的話,得將會議弄的輕鬆一點。
  • Pair Rotating
目前我們 team 已經自動 pair 起來了,下一個目標自然是 pair rotating。這個遠大的目標不知何時可以採用?還是先玩個 pair 兩三個月好了,等大家非得 pair 時,再來試試看。

Simplied Struts DispatchAction

Struts 的 Dispatch Action 已經行之有年了,有 Dispatch, Lookup, MappingDispatch... 好幾種可以用。但設定方法都很囉嗦 (網頁,程式,struts-config三者都要 hard-code String 在上面),而且適用的了 A ,就不能用在 B。例如雖然 DispatchAction 可以寫在 URL 上: /foo.do?method=saveOrder ,但是遇到一個 form 需要利用多個 submit button dispatch 時,就不行了。你得轉用 LookupDispatch,可是 LookupDispatch 又需要在 Action 定義一個很醜的 getKeyMethodMap() (裡面全部是 hard-code string)。接下來要分享一個通用 URL/submit button/struts-config.xml 三者設定的 SimpleDispatchAction:

package org.bioinfo.util.struts;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.springframework.web.struts.ActionSupport;
/**
* 簡化的 DispatchAction,並支援Spring的 ActionSupport (如不需要可以換回 Action)
*
* 使用方法:
*
* 繼承此 class,並定義各個 action method,比方說有兩個 method 分別做儲存和刪除:
*
* <code>
* public ActionForward save(ActionMapping mapping, ActionForm form,
* HttpServletRequest request, HttpServletResponse response)
* throws Exception {
* // orderService.save(....)
* }
*
* public ActionForward delete(ActionMapping mapping, ActionForm form,
* HttpServletRequest request, HttpServletResponse response)
* throws Exception {
* // orderService.delete(....)
* }
* </code>
*
* 有三種方式可以設定 dispatch 到哪個 method 上。
*
* (1) submit button 法,直接寫在 submit 的 property 上:
*
* <code>
* <html:form action="/some/work">
* .... some thing ....
*
* <html:submit property="dispatch=save" value="儲存"/>
* <html:submit property="dispatch=delete" value="刪除"/>
* </html:form>
* </code>
*
* 注意 property 裡面的值是 'dispatch=xxxx' 記得要寫等號與 method 名稱,
* 而且大小寫要對,不能空白。當網頁按下 "儲存" 時,則會執行 /some/work.do
* 的 save(...) 的 method。 如果按下 "刪除" 則執行 delete(...),
*
* 建議 -- 這種寫法通常是用在一個 Action 有多個 dispatch method,而每個 method
* 都共用同個 ActionForm
*
* (2) URL 法,接在 URL 後面:
*
* <code>
* <html:form action="/some/work?dispatch=save">
* 或是用 link 也可以
* <html:link action="/some/work?dispatch=save" />
* </code>
*
* 建議 -- 通常用在不需要 ActionForm 的 Action,或者是要將 submit button 法
* 寫成 url 時使用。
*
* (3) struts-config 法,直接寫死在 parameter='dispatch=foo' 上
*
* <code>
* <action
* path="/saveOrder"
* name="SaveOrderForm"
* type="antar.order.web.OrderDispatchAction"
* parameter="dispatch=save" >
* </action>
* <action
* path="/deleteOrder"
* name="DeleteOrderForm"
* type="antar.order.web.OrderDispatchAction"
* parameter="dispatch=delete" >
* </action>
* </code>
*
* 建議 -- 這種寫法通常是為了讓 Action 中每個 dispatch method 使用不同
* 的 ActionForm。一旦寫死在 struts-config 裡,該 mapping 的 path 就不能
* 與 URL 法 或是 submit button 法同時使用。
*
* 最後請注意同一個 request 上,URL 法不能與 submit button 法同時使用:
*
* <code>
* ...........錯誤範例...........
* <html:form action="/some/work?dispatch=save">
* .... some thing ....
* <html:submit property="dispatch=delete" value="刪除"/>
* </html:form>
* </code>
*
* @author ingram
*
*/
public abstract class SimpleDispatchAction extends ActionSupport {

private static final String KEY_VALUE_SEPERATOR = "=";

private final static String KEY = "dispatch";

private static final String VALID_PARAMETER_NAME_PATTERN = KEY + "\\"
+ KEY_VALUE_SEPERATOR + "[a-zA-Z0-9_]+";

private Class clazz = this.getClass();

private Class[] argTypes = new Class[] { ActionMapping.class,
ActionForm.class, HttpServletRequest.class,
HttpServletResponse.class };

private Map dispatchMethods = new HashMap();

public final ActionForward execute(ActionMapping mapping,
ActionForm form, HttpServletRequest request,
HttpServletResponse response) throws Exception {

String methodName = getMethodName(request, mapping);

Method method = null;
try {
Object[] args = { mapping, form, request, response };
method = obtainDispatchMethod(methodName);
return (ActionForward) method.invoke(this, args);
} catch (NoSuchMethodException e) {
throw dealWithMethodProblem(methodName, e);
} catch (IllegalAccessException e) {
throw dealWithMethodProblem(methodName, e);
} catch (InvocationTargetException e) {
throw dealWithMethodProblem(methodName, e);
}

}

private RuntimeException dealWithMethodProblem(String methodName,
Exception e) {
return new RuntimeException(
"can not access dispatching method:["
+ methodName
+ "]. ", e);
}

private Method obtainDispatchMethod(String methodName)
throws NoSuchMethodException {
Method method = (Method) dispatchMethods.get(methodName);
if (method == null) {
method = clazz.getMethod(methodName, argTypes);
dispatchMethods.put(methodName, method);
}
return method;
}

static String getMethodName(HttpServletRequest request,
ActionMapping mapping) {

final List gatherAllMethodNames = new ArrayList();
for (Enumeration e = request.getParameterNames();
e.hasMoreElements();) {
String parameterName = (String) e.nextElement();
addMatchedParameter(gatherAllMethodNames, parameterName);
}

final String[] values = request.getParameterValues(KEY);
if (values != null) {
for (int i = 0; i < values.length; i++) {
gatherAllMethodNames.add(values[i]);
}
}

if (mapping.getParameter() != null) {
addMatchedParameter(gatherAllMethodNames
, mapping.getParameter());
}

if (gatherAllMethodNames.isEmpty()) {
throw new IllegalArgumentException(
"no 'dispatch=methodName' found in parameter");
} else if (gatherAllMethodNames.size() > 1) {
throw new IllegalArgumentException(
"\nMultiple dispatch parameter: " + gatherAllMethodNames
+ " Only one parameter is allowed.");
} else {
return (String) gatherAllMethodNames.iterator().next();
}
}

private static void addMatchedParameter(List gatherAllMethodNames,
String parameterName) {
if (parameterName.matches(VALID_PARAMETER_NAME_PATTERN)) {
gatherAllMethodNames
.add(parameterName.split(KEY_VALUE_SEPERATOR)[1]);
}
}
}
嘿嘿,有了這個統合的 Dispatch,日子就好過多啦!我也另外用同樣的邏輯寫了個 SimpleDispatchActionForm,搭配起來用不錯。

星期六, 4月 16, 2005

靠!EasyMock

靠!真的想自殺,居然誤解了 EasyMock 這麼久!
長久以來,寫 mock 我都是一直在 test case 裡寫個 static inner mock class,供各個 test 使用。當初學這套方法時也調查了很多 tool,但是都沒有完美的 solution,當時的 easymock 只能套在 interface 上,完全不合用。而 Dynamock / jMock 族系的需要繼承特殊的 TestCase,而且他的 "錄製" 過程中呼叫 method 是用 method name 的,像這樣:

mockSubscriber.expects(once()).method("receive").with( eq(message) );

天啊,Subscriber.receive() 的呼叫是用 String,這樣 refactor method name 時不就掛了!!而且真的很難看懂他在幹嘛。所以... 就打消使用 mock tool 的念頭了...
昨個亂逛居然發現 easymock 已經有 extension 可以用在 class 上了!而且還是去年就有了!看看下面的這幾行標準的 easymock 使用:

private MockControl control;

private OrderDAO mockOrderDAO;

protected void setUp() throws Exception {
//MockClassControl 可以替 class 建立 Mock
control = MockClassControl.createStrictControl(OrderDAO.class);
mockOrderDAO = (OrderDAO) control.getMock();
}

public void testSaveOrder() {
//開始預錄
Order order = new Order();
//執行真正的 method 來錄製,而不是用 method name
mockOrderDAO.saveOrder(order);
control.replay();
//錄製完成

OrderServiceImpl service = new OrderServiceImpl();
service.setOrderDAO(mockOrderDAO);
service.saveCustomerOrder(order);
//比對錄製結果
control.verify();
}
就是這麼簡單,唯一的限制是需要非 final 的 public constructor,不過這是小事啦!
居然一直停留在 mock tool 限制很多的印象裡... 該死!

靠~~~~