<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss'><id>tag:blogger.com,1999:blog-6833744</id><updated>2009-09-10T13:01:22.231+08:00</updated><title type='text'>Xexex's Java 和其他二三事 (舊)</title><subtitle type='html'>The page is mainly for sharing Java experience.
再加上一些不知所云的二三事。
而且，如你所見，內容是半中半英的</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default?start-index=26&amp;max-results=25'/><author><name>ingramchen</name><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>42</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-6833744.post-112091469688532496</id><published>2005-07-09T21:02:00.000+08:00</published><updated>2005-07-09T21:32:48.656+08:00</updated><title type='text'>Blog 搬家囉，這個地方不會再使用了.....</title><content type='html'>&lt;pre&gt;&lt;br /&gt; /**&lt;br /&gt;  * @deprecated 本 blog 已經停用請改用下面的網址&lt;br /&gt;  *      &lt;br /&gt;  * @link 新Blog：&lt;a href="http://www.javaworld.com.tw/roller/page/ingramchen"&gt;JavaWorld jroller&lt;/a&gt;&lt;br /&gt;  */&lt;br /&gt;  public void viewXexexBlog() ;&lt;br /&gt;&lt;/pre&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-112091469688532496?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/112091469688532496/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=112091469688532496' title='1 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/112091469688532496'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/112091469688532496'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/07/blog.html' title='Blog 搬家囉，這個地方不會再使用了.....'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-112065544916728149</id><published>2005-07-06T21:04:00.000+08:00</published><updated>2005-07-07T09:56:58.326+08:00</updated><title type='text'>XP programming !</title><content type='html'>我們新專案已完成 phase I 了，爽！&lt;br /&gt;&lt;br /&gt;&lt;a href="http://www.flickr.com/photos/xexex/24038571/" title="Photo Sharing"&gt;&lt;img src="http://photos18.flickr.com/24038571_857501225f.jpg" width="500" height="377" alt="buildhistory" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;這 張圖是我們 (六個人) 三個月來 pair 的成果，Y軸是每天 continuous integration 的 test case 的數量 (yes, 每天都是 success build)。四月初到四月中旬都是在分析需求，所以沒有增加 testcase。四月中開始則每天以20到30個 test case 不等，持續穩定的增加。最後七月總共1200 個 unit test。空白的部份則是週休二日。&lt;br /&gt;&lt;br /&gt;雖然說穩定增加，其實裡面有兩週 (五月初和六月初)線是水平的。五月初那次是寫到400個test case 後發現架構有問題，所以整週都在做 refactoring (事後證明 refactoring 是對的) 六月初那次是大家去三天員工旅遊的啦~~~ 所以線也是平的 :-)&lt;br /&gt;&lt;br /&gt;專案的平台是 Struts + Hibernate + Spring，除了JSP 之外，全部都寫了 Unit case。不知這樣的架構三個月 1200 個 test case 會不會算太少太慢？網路上從來沒看過相關文獻說....&lt;br /&gt;&lt;br /&gt;我 們第一個專案 是 Struts + Hibernate平台，共 1500 個 Test Case，當時 Struts 並沒有寫 Unit Test，所以大多數是 business, database 相關的 test case。你猜總共要跑多久？總共要 40 分鐘！寫到後來每次都要等很久..... 沒力啊.... 而這新的專案呢？ 1200 個 只需要 4 分鐘 (包含建立資料庫) ，非常非常地快，開發的節奏完全不一樣了，讚！這都歸功於 Spring, Easymock, 與新版 Ant 的功能，強力推薦大家使用！&lt;br /&gt;&lt;br /&gt;pair 的成員上次提過了，三個男 + 三個女的 (我們這男女開發員比例約為 1:1)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;第一組：&lt;br /&gt;       (主導性高) 重度感染 Test 的 男 developer (症狀是每天逼大家寫 Test)&lt;br /&gt;       (主導性低) 中間程度之男Developer (但不熟. Hibernate /Unit test ... )&lt;br /&gt;&lt;br /&gt;第二組：&lt;br /&gt;       (主導性高) 正值懷孕的中上程度女 developer (我沒寫錯，你也沒看錯，是 "懷孕")&lt;br /&gt;       (主導性低) 中間程度之女  Developer  (配合度高)&lt;br /&gt;&lt;br /&gt;第三組：&lt;br /&gt;       (主導性中) 男新人，不過底子夠，自學性強&lt;br /&gt;       (主導性中) 女 java 新手，背景是 procedure, database SQL 出身，不過她是整個 team 裡最熟 domain 的。&lt;br /&gt;&lt;br /&gt;第一組曾發生過內亂，"Test 男" 嫌 "不熟男" 程度跟不上。氣的想自己寫算了。後來 "不熟男"發奮圖強，所以暫時先合解。"Test 男" 也改變了做法，pair 的時候都讓 "不熟男" 操作鍵盤，好好訓練。現在 pair 經過三個月後，"不熟男" 漸漸有點成績出來了。雖說獨當一面還不大夠，但是已經可以獨立維護程式了，終於變 "熟男" 了。&lt;br /&gt;&lt;br /&gt;第二組在技術和經驗都沒什麼問題啦，她們後來也寫了最多的 test case。比較大的問題是 "懷孕女" 正值懷孕期間.... 還好跟她搭當的是我們 team 中配合度極佳的 女 developer，所以一切相安無事。不過 "配合女" 由於鍵盤都被主導性極強的 "懷孕女"搶走，所以對 pair programming 不是有好感，還是希望能自己寫，有自己的發揮空間。&lt;br /&gt;&lt;br /&gt;第三組是個人最擔心的一 組... 因為兩個人都是第一次在這裡開發 java 專案，前兩組的都有過舊專案的 "教訓"，所以都會採取比較正確的寫法。三個月下來，雖然 code 有一些暇疵，不過還是漂亮的完成了任務。"底子夠男" 脾氣比 "Test 男" 好多了，跟新手 pair 比較有耐心。"女java新手" 除了 web 那些有的沒的學不大起來之外，其他的邊學邊寫之下也頗成氣候的。而且她對 domain 熟，抓的方向會比較準。有趣的是 "女 java 新手" 開發完後的結論是："還是 procedure 好寫......"&lt;br /&gt;&lt;br /&gt;如果這次不是這樣 pair，比方說改成 "Test 男" + "女java新手"，也許 "女java新手" 會比較了解為何 java 要這麼複雜，為何所有程式都要寫 test。又如將 "不熟男" + "配合女" 一起 pair，那自然就不會有衝突，"配合女" 也拿的到鍵盤，比較有機會發揮了。再將 "懷孕女" + "底子夠男" pair，那麼這一組的程式就不會有暇疵，"底子夠男" 可直接吸收 "懷孕女" 的經驗，而 "底子夠男" 脾氣較好，比較不會影響 "懷孕女" 的胎氣...&lt;br /&gt;&lt;br /&gt;當然這樣想是事後諸葛啦，誰知道這樣的 pair 會不會有新的問題咧？&lt;br /&gt;&lt;br /&gt;(上面那一段用的詞句有點奇怪，希望大家看的懂啦...)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Anyway, 個人對這次 pair 的感受是 -- &lt;br /&gt;&lt;br /&gt;(1) pair programming 是最好的教育訓練的工具。原本高中低手都有，三個月後大家的水準馬上都拉上來了。未來我想 pair 的配對選擇方式應該會以教育訓練為第一個考量。&lt;br /&gt;&lt;br /&gt;(2) pair programming 減輕很多壓力！不論是設計或是撰寫，你都有 partner 可以商量，分擔責任，我們 team 裡沒有專職的 SA/SD，所以 pair design 適時的發揮效用。另外，想請假也有 "完整" 代理人。而且你不用花時間維護太多的文件，你隨時都有接班人，文件自然能寫精簡一點啦&lt;br /&gt;&lt;br /&gt;(3) 不用說，code quality 自然是標準水平以上了。如果這個專案是六個人分開寫，我真的不知道會變成什麼樣子... 就算三個月生東西出來了，大概也不能用吧....&lt;br /&gt;&lt;br /&gt;總結。pair programming 不可能風平浪靜.... 有很多問題一一待克服.... 我們這先天的優點是男女比例一樣，我想如果是清一色的男性，那困難會大上很多。三個月下來，我相信我們 team 已經跨過 XP 的門檻了，相信未來的專案會繼續採用 XP -- 因為我們已經嘗到甜頭啦！&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-112065544916728149?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/112065544916728149/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=112065544916728149' title='3 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/112065544916728149'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/112065544916728149'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/07/xp-programming.html' title='XP programming !'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111615174848268819</id><published>2005-05-15T17:31:00.000+08:00</published><updated>2005-05-15T18:53:09.610+08:00</updated><title type='text'>use AOP to simplify hibernateTemplate (Hibernate3)</title><content type='html'>Spring ORM 替 Hibernate 的 API (Session and Query) 做了一番修整，也就是 HibernateTemplate ，它最主要的目的是 resource  控管及 exception 的轉換。有了這個 persistence layer 的 code 就可以大量的減少並且降低出錯的機會。然而這個 API 卻抹剎了 Hibernate API 的簡單易用優點，舉例來說，如果藉由 hibernateTemplate 控制 Hibernate Session (ex. Criteria)，唯一的做法就是寫很難看的 Callback:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public MyData findByNo(final String no) {&lt;br /&gt; final String hql = " select data from MyData data "&lt;br /&gt;   + " where data.no = :no ";&lt;br /&gt;    &lt;br /&gt; HibernateCallback hc = new HibernateCallback() {&lt;br /&gt;  public Object doInHibernate(Session session) {&lt;br /&gt;   Query q = session.createQuery(hql);&lt;br /&gt;   q.setString("no", no);&lt;br /&gt;   return q.uniqueResult();&lt;br /&gt;  }&lt;br /&gt; };&lt;br /&gt;      &lt;br /&gt; return (MyData) hibernateTemplate.execute(hc);&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;inner class +  final.... 有夠難看。下面要介紹的是，利用 Spring AOP 的 AutoProxy 的功能，自動將 DAO 的所有 method 用 HibernateCallback 包起來。如此一來，便可以自由在 DAO 中自由存取 Hibernate Session 了：&lt;br /&gt;&lt;br /&gt;總共需要三個 class:&lt;br /&gt;第一個是 Advice，也就是將所有 method 都先用 HibernateCallback 包起來，然後將 Hibernate Session 放入 HibernateDAOWarpper 裡。&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class HibernateDAOWrapperMethodInterceptor &lt;br /&gt;                                      implements MethodInterceptor {&lt;br /&gt;&lt;br /&gt;    private HibernateTemplate hibernateTemplate;&lt;br /&gt;&lt;br /&gt;    public Object invoke(final MethodInvocation methodInvocation)&lt;br /&gt;            throws Throwable {&lt;br /&gt;&lt;br /&gt;        try {&lt;br /&gt;            return hibernateTemplate.execute(new HibernateCallback() {&lt;br /&gt;&lt;br /&gt;                public Object doInHibernate(Session session)&lt;br /&gt;                        throws HibernateException, SQLException {&lt;br /&gt;                    HibernateDAOWrapper.currentSession.set(session);&lt;br /&gt;                    try {&lt;br /&gt;                        return methodInvocation.proceed();&lt;br /&gt;                    } catch (Throwable e) {&lt;br /&gt;                        if (e instanceof HibernateException) {&lt;br /&gt;                            throw (HibernateException) e;&lt;br /&gt;                        } else if (e instanceof SQLException) {&lt;br /&gt;                            throw (SQLException) e;&lt;br /&gt;                        } else {&lt;br /&gt;                            //must re-throw as RuntimeException, and let&lt;br /&gt;                            //hibernateTemplate deal with resource management&lt;br /&gt;                            throw new ExceptionCarrier(e);&lt;br /&gt;                        }&lt;br /&gt;                    }&lt;br /&gt;                }&lt;br /&gt;&lt;br /&gt;            });&lt;br /&gt;        } catch (ExceptionCarrier e) {&lt;br /&gt;            throw e.throwable;&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    private static class ExceptionCarrier extends RuntimeException {&lt;br /&gt;        private Throwable throwable;&lt;br /&gt;&lt;br /&gt;        public ExceptionCarrier(Throwable t) {&lt;br /&gt;            this.throwable = t;&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {&lt;br /&gt;        this.hibernateTemplate = hibernateTemplate;&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;接下來就是 AutoProxyCreator，當它遇到 bean 的 class 為 HibernateDAOWrapper 的 subclass 時，它就會自動替該 bean 做 proxy。&lt;pre&gt;&lt;br /&gt;public class HibernateDAOWrapperAutoProxyCreator extends&lt;br /&gt;        AbstractAutoProxyCreator {&lt;br /&gt;&lt;br /&gt;    protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass,&lt;br /&gt;            String beanName, TargetSource customTargetSource)&lt;br /&gt;            throws BeansException {&lt;br /&gt;        if (HibernateDAOWrapper.class.isAssignableFrom(beanClass)) {&lt;br /&gt;            return PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS;&lt;br /&gt;        } else {&lt;br /&gt;            return DO_NOT_PROXY;&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;有了這個，再搭配上面的 Advice，我們就可以將所有 HibernateDAOWrapper 的 subclass 的所有 method 都用 HibernateCallback 包起來。Spring 的設定如下：&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;&amp;lt;bean id=&amp;quot;hibernateDAOWrapperAutoProxyCreator&amp;quot;&lt;br /&gt; class=&amp;quot;org.bioinfo.util.spring.dao.HibernateDAOWrapperAutoProxyCreator&amp;quot;&amp;gt;&lt;br /&gt; &amp;lt;property name=&amp;quot;interceptorNames&amp;quot;&amp;gt;&lt;br /&gt;  &amp;lt;list&amp;gt;&lt;br /&gt;   &amp;lt;value&amp;gt;hibernateDAOWrapperMethodInterceptor&amp;lt;/value&amp;gt;&lt;br /&gt;  &amp;lt;/list&amp;gt;&lt;br /&gt; &amp;lt;/property&amp;gt;&lt;br /&gt; &amp;lt;!-- for use CGlib to create proxy, &lt;br /&gt;       (default to false, which will use Proxy if it detects interface presence) --&amp;gt;&lt;br /&gt; &amp;lt;property name=&amp;quot;proxyTargetClass&amp;quot;&amp;gt;&lt;br /&gt;       &amp;lt;value&amp;gt;true&amp;lt;/value&amp;gt;&lt;br /&gt; &amp;lt;/property&amp;gt;&lt;br /&gt;&amp;lt;/bean&amp;gt;     &lt;br /&gt;&amp;lt;bean id=&amp;quot;hibernateDAOWrapperMethodInterceptor&amp;quot;&lt;br /&gt; class=&amp;quot;org.bioinfo.util.spring.dao.HibernateDAOWrapperMethodInterceptor&amp;quot;&amp;gt;&lt;br /&gt; &amp;lt;property name=&amp;quot;hibernateTemplate&amp;quot;&amp;gt;&lt;br /&gt;  &amp;lt;ref local=&amp;quot;hibernateTemplate&amp;quot;/&amp;gt;&lt;br /&gt; &amp;lt;/property&amp;gt;&lt;br /&gt;&amp;lt;/bean&amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;這個只要設一次就夠了。接下來我們再來看看主角：&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;/**&lt;br /&gt; * A special HibernateTemplate wrapper for DAO. You need to do:&lt;br /&gt; * &lt;br /&gt; * (1) Extends this class and use getSession() to obtain Hibernate3 Session&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; * public class YourDAO extends HibernateDAOWrapper {&lt;br /&gt; * &lt;br /&gt; *     public MyProject findByProjectCode(String code) {&lt;br /&gt; *         String hql = "select prj from MyProject prj where prj.code = :code";&lt;br /&gt; * &lt;br /&gt; *         return (MyProject) getSession().createQuery(hql)&lt;br /&gt; *                      .setString("code",code").uniqueResult();&lt;br /&gt; *     }&lt;br /&gt; * }&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * (2) At your spring *.xml, just write one line:&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; * &amp;lt;bean id="yourDAO" class="some.thing.YourDAO"&amp;gt;&amp;lt;/bean&amp;gt;&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * done !!&lt;br /&gt; * @author ingram&lt;br /&gt; *  &lt;br /&gt; */&lt;br /&gt;public abstract class HibernateDAOWrapper {&lt;br /&gt;&lt;br /&gt;    final static ThreadLocal currentSession = new ThreadLocal();&lt;br /&gt;&lt;br /&gt;    protected final Session getSession() {&lt;br /&gt;        return (Session) currentSession.get();&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;呵，小的可憐，它的功能就是提供 getSession() 的 method 給 subclass 的 DAO 而已。這個 session 來自於 Advice，並且放在 ThreadLocal裡，因此是 thread-safe 的。使用方式就如上面的 javadoc 所說，將 DAO 直接 subclass 這個 Wrapper，並在 spring 裡定義即可。&lt;br /&gt;&lt;br /&gt;這樣一來 DAO 就可以快快樂樂的用 Hibernate API 啦，爽！&lt;br /&gt;&lt;br /&gt;(註：僅適用 Hibernate3，於 Hibernate 3.0.3 + Spring 1.2 final 測試 )&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111615174848268819?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111615174848268819/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111615174848268819' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111615174848268819'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111615174848268819'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/05/use-aop-to-simplify-hibernatetemplate.html' title='use AOP to simplify hibernateTemplate (Hibernate3)'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111605147334552961</id><published>2005-05-14T14:00:00.000+08:00</published><updated>2005-05-15T22:07:31.080+08:00</updated><title type='text'>(draft v2) Best Practice for Struts+Hibernate+Spring</title><content type='html'>最近又看了一本 Pro Spring，喔~ 裡面的第十一章 (Designing and implementating spring-based applications) 真是太棒囉！很多建議都相當的實用，解決了許多困惑我很久的疑問。這樣子差不多可以整理出一個通用的開發架構了：&lt;br /&gt;&lt;br /&gt;o &lt;span style="font-weight: bold;"&gt;J2EE without EJB&lt;/span&gt;&lt;br /&gt;採用 Struts + Hibernate +Spring 三個 framework 開發專案，每個framework都提供較 j2ee 更簡單、更快速、更open的實作。&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Struts 提供 presentation layer 的各項服務&lt;/li&gt;   &lt;li&gt;Hibernate 提供 domain layer 的 persistence 及 query 服務&lt;/li&gt;   &lt;li&gt;Spring 提供 transaction/mail/remoting... 等等 middle-ware 的服務&lt;/li&gt; &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Writing unit test for everything&lt;/span&gt;&lt;br /&gt;Struts Action/ActionForm, Hibernate DAO, Spring Service.... etc 從上到下所有的 java code 都依循 TDD 的方式開發。目前除了 jsp 之外，全部都要寫 unit test。&lt;br /&gt;&lt;br /&gt;o &lt;span style="font-weight: bold;"&gt;4 layers&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Presentation Layer - 與使用者互動&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;Application Service Layer - 此層可明確表示 use case / user story / user requirement&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;Domain Layer - domain problem mapping as domain objects interaction.&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;Persistence Layer - 負責物件的儲存及查詢. (物件的中間生命週期)&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;保持 domain layer 乾淨&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;       no DAO&lt;/li&gt;   &lt;ul&gt;     &lt;li&gt;進入 domain layer 前，先將資料轉換為 domain object，無論是從 database 來或是使用者輸入。&lt;br /&gt; &lt;/li&gt;     &lt;li&gt;domain object 間的 association，如果需要從 database query，盡可能改用 ORM 直接做associate ，而不是再透過 DAO 才連結。(除非 performance 的考量)&lt;br /&gt;&lt;/li&gt;   &lt;/ul&gt; &lt;/ul&gt; &lt;ul&gt;       &lt;/ul&gt; &lt;ul&gt;   &lt;li&gt;         no Service&lt;/li&gt;   &lt;ul&gt;     &lt;li&gt;domain object 不應與任何 framework, container, context, enviroment... etc 等相關。所有存在於Domain Layer 的物件僅僅是互相連結的 POJO。&lt;br /&gt; &lt;/li&gt;   &lt;/ul&gt; &lt;/ul&gt; o &lt;span style="font-weight: bold;"&gt;Domain Object &gt;= Persistence Object&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Domain Object is not nessasary identical with Persistence Object(PO). just choose domain objects that need to persistence and query as PO.&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;All Persistence Object's getter/setter should be in default package scope.&lt;/li&gt; &lt;/ul&gt; o &lt;span style="font-weight: bold;"&gt;Design Domain Object fine grain and immutable&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Do not afraid introduceing new Class.&lt;/li&gt;   &lt;li&gt;Prefer immutable object. Immutable Object enforce you mapping domain problem to domain object correctly, and reduce maintain effort. Only Persistence Object has mutable characteristic.&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;no getter/setter. Object's property is hidden and only expose behavior to public.&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;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.&lt;/li&gt; &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Domain Object 跨全域&lt;br /&gt;&lt;/span&gt; &lt;ul&gt;   &lt;li&gt;Domain Object 會傳到 presentation layer 與使用者互動，會傳到 application layer 使用 middle-ware 服務、會扮演 PO 進入 persistence layer 儲存。&lt;br /&gt;  &lt;/li&gt;   &lt;li&gt;安全地跨全域的關鍵在於 Immutable。&lt;/li&gt;   &lt;li&gt;DTO is non-sense.&lt;br /&gt;  &lt;/li&gt; &lt;/ul&gt; o &lt;span style="font-weight: bold;"&gt;Classify Root of Domain Object in domain layer&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;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.&lt;/li&gt;   &lt;li&gt;ex. An engine Factory produce various engines and sell them to down-stream factory. Obviously now the engine is Root class for this domain.&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Service Layer:&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;thin business logic&lt;/li&gt;   &lt;li&gt;coordinate:&lt;/li&gt;   &lt;ul&gt;     &lt;li&gt;Root of Domain Object&lt;/li&gt;     &lt;li&gt;DAO&lt;/li&gt;     &lt;li&gt;Other middle-ware service (mail/remoting/transaction)&lt;/li&gt;   &lt;/ul&gt;   &lt;li&gt;each method is unit of work of user's single operation&lt;/li&gt;   &lt;li&gt;method boundary is transaction boundary, declare in Spring.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Spring managed resources are all in this layer.&lt;/li&gt;&lt;li&gt;Apply IoC in this layer heavily to make test easier. (flexible design)&lt;/li&gt;   &lt;li&gt;Every service is a interface and pair with at least one implementation: FooService/FooServiceImpl&lt;br /&gt; &lt;/li&gt;   &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Persistence Layer:&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;      &lt;li&gt;Not all database table has it's own DAO, only Root of Domain Object has DAO&lt;/li&gt;   &lt;li&gt;See Hibernate 3 Reference chap. 7 to choose best association for your ORM&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;Efficiently use Hibernate's optimistic lock feature for concurrecy issue.&lt;/li&gt;   &lt;li&gt;Efficiently use Hibernate's lazy/cache/fetch join technique to improve ORM performance&lt;/li&gt;   &lt;li&gt;All POs have an primary key: 'Long id'. DO NOT use compose key !&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;All POs' getter/setter scope is package default (private is better)&lt;/li&gt;&lt;li&gt;Perfer HibernateTemplate and JdbcTemplate, with this we don't require additional interface for DAOs.&lt;br /&gt; &lt;/li&gt;  &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Struts ActionForm:&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;ActionForm will be populated from Domain Object for user input.&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;After user input, the ActionForm should convert user's input back to domain object if possible.&lt;/li&gt;&lt;li&gt;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.&lt;br /&gt;&lt;/li&gt;  &lt;/ul&gt;o &lt;span style="font-weight: bold;"&gt;Struts Action:&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;each Action only call one method of a single service. (because of transaction boundary)&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;Action's responsibility is to coordinate Invoking service method, ActionForm population, ActionForward flow, and compose ActionMessage... etc, nothing more.&lt;br /&gt;&lt;/li&gt;&lt;li&gt;does nothing about business logic.&lt;br /&gt;&lt;/li&gt;  &lt;/ul&gt; o &lt;span style="font-weight: bold;"&gt;Design Business Exception:&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;All business exceptions are checked and user recoverable. if the exception is user un-recoverable, it should be classified as RuntimeException&lt;/li&gt;   &lt;li&gt;All business exception should extends 'BusinessException', the Root of all business exceptions&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;If you want your business exception support i18n, use 'ResourceKeyBusinessException', which extends from 'BusinessException'&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;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&lt;/li&gt;&lt;li&gt;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.&lt;/li&gt;  &lt;/ul&gt;好像越寫越多... 看起來亂亂的...  而且英文佔了一大半 -_-;&lt;br /&gt;啊，就先這樣就好了，以後有空在補。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111605147334552961?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111605147334552961/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111605147334552961' title='4 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111605147334552961'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111605147334552961'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/05/draft-v2-best-practice-for.html' title='(draft v2) Best Practice for Struts+Hibernate+Spring'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>4</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111485297415782661</id><published>2005-04-30T17:17:00.000+08:00</published><updated>2005-04-30T17:22:54.156+08:00</updated><title type='text'>反微軟終結</title><content type='html'>這個站是我見過最久的網站...  (BBS 不算) 終於停了...&lt;br /&gt;哎... 微軟什麼時候才會倒...&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111485297415782661?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111485297415782661/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111485297415782661' title='1 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111485297415782661'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111485297415782661'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/blog-post.html' title='反微軟終結'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111436122238769781</id><published>2005-04-24T23:53:00.000+08:00</published><updated>2005-04-25T01:26:25.540+08:00</updated><title type='text'>Pair Programming</title><content type='html'>去年以來個人一直在 team 裡疾呼 unit-test 的重要。現在整個 team 裡除了幾個不曾跟 heavy-tester 一起合作的人之外，幾乎都了解 test 的好處和重要 (有些人是沒寫test 吃了大虧，有些人先是被逼著寫，後來嘗到了甜頭 )。去年的開發幾乎完全都是個人單打獨鬥，然後單純互相討論，今年這個新專案由六個人組成，好玩的是分工完之後就自動兩兩成對 pair 了。記得去年前 pair 時大家都哇哇叫，今年好像因為個別的不同理由不得不 pair 啊，目前 pair 的分配是：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;   pair A -- 資深(主導性強) + test 新手&lt;/li&gt;   &lt;li&gt;   pair B -- 資深(主導性強) + 中等&lt;/li&gt;   &lt;li&gt;   pair C -- java 新手 + test 新手  (兩人主導性差不多)&lt;/li&gt; &lt;/ul&gt; 上面的資深資淺以 test 的資歷來計，我個人認為 unit-test 的 quality 很適合用來判斷一個 developer 的生產力。&lt;br /&gt;&lt;br /&gt;A組沒有 pair 跟本做不下去，完全由資深帶領資淺，以免出差錯，不過資深的步調極快，資淺的步調極慢.........&lt;br /&gt;&lt;br /&gt;C組的只能 pair 了，因為其中一人剛學 java.... 另一人功力夠，不過還沒實際作過專案，也沒有 test 的經驗&lt;br /&gt;&lt;br /&gt;B組的都有 test 經驗，而且都能夠獨力作業，其實不 pair 好像也沒差，不過看起來 pair 的理由是有人可以互相討論，減輕壓力。&lt;br /&gt;&lt;br /&gt;這是現在看到的現象，不知再過三個月會變成如何呢？呵呵，接下來討論一下經驗和構想吧：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Personality&lt;/li&gt; &lt;/ul&gt; 我發現讓兩個主導性強的或是兩個主導性弱的 pair 在一起問題會很多。兩個主導性強的會堅持己見，很容易吵在一起，不僅氣氛差，進度也會變慢。兩個弱的則是遇到問題會卡很久，然後慢吞吞的，不曉得東西什麼時候會出來，很危險。&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Relax Time Control&lt;/li&gt; &lt;/ul&gt; 這個..... 我們目前完全沒有控制作息的時間，目前我的構想是 break 個兩次。讓兩人休息休息，下個禮拜建議大家實施看看好了。&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Standup Meeting&lt;/li&gt; &lt;/ul&gt; XP 的早上一開始都會開個幾分鐘的小會議，目前我們也還沒有試過，不過這個大概很難吧... 台灣人好像很不喜歡在制式的會議上發言.... 大家覺得能躲就躲.... 如果這個要推行的話，得將會議弄的輕鬆一點。&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Pair Rotating&lt;/li&gt; &lt;/ul&gt; 目前我們 team 已經自動 pair 起來了，下一個目標自然是 pair rotating。這個遠大的目標不知何時可以採用？還是先玩個 pair 兩三個月好了，等大家非得 pair 時，再來試試看。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111436122238769781?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111436122238769781/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111436122238769781' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111436122238769781'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111436122238769781'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/pair-programming.html' title='Pair Programming'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111431546963091310</id><published>2005-04-24T11:44:00.000+08:00</published><updated>2005-04-24T12:22:13.206+08:00</updated><title type='text'>Simplied Struts DispatchAction</title><content type='html'>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:&lt;pre&gt;&lt;br /&gt;package org.bioinfo.util.struts;&lt;br /&gt;&lt;br /&gt;import java.lang.reflect.InvocationTargetException;&lt;br /&gt;import java.lang.reflect.Method;&lt;br /&gt;import java.util.ArrayList;&lt;br /&gt;import java.util.Enumeration;&lt;br /&gt;import java.util.HashMap;&lt;br /&gt;import java.util.List;&lt;br /&gt;import java.util.Map;&lt;br /&gt;&lt;br /&gt;import javax.servlet.http.HttpServletRequest;&lt;br /&gt;import javax.servlet.http.HttpServletResponse;&lt;br /&gt;&lt;br /&gt;import org.apache.struts.action.ActionForm;&lt;br /&gt;import org.apache.struts.action.ActionForward;&lt;br /&gt;import org.apache.struts.action.ActionMapping;&lt;br /&gt;import org.springframework.web.struts.ActionSupport;&lt;br /&gt;/**&lt;br /&gt; * 簡化的 DispatchAction，並支援Spring的 ActionSupport (如不需要可以換回 Action)&lt;br /&gt; * &lt;br /&gt; * 使用方法：&lt;br /&gt; * &lt;br /&gt; * 繼承此 class，並定義各個 action method，比方說有兩個 method 分別做儲存和刪除：&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; * public ActionForward save(ActionMapping mapping, ActionForm form,&lt;br /&gt; *         HttpServletRequest request, HttpServletResponse response)&lt;br /&gt; *         throws Exception {&lt;br /&gt; * // orderService.save(....) &lt;br /&gt; * }&lt;br /&gt; * &lt;br /&gt; * public ActionForward delete(ActionMapping mapping, ActionForm form,&lt;br /&gt; *         HttpServletRequest request, HttpServletResponse response)&lt;br /&gt; *         throws Exception {&lt;br /&gt; * // orderService.delete(....) &lt;br /&gt; * }&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * 有三種方式可以設定 dispatch 到哪個 method 上。&lt;br /&gt; * &lt;br /&gt; * (1) submit button 法，直接寫在 submit 的 property 上：&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; * &amp;lt;html:form action="/some/work"&amp;gt;&lt;br /&gt; *    .... some thing ....&lt;br /&gt; * &lt;br /&gt; *    &amp;lt;html:submit property="dispatch=save" value="儲存"/&amp;gt;&lt;br /&gt; *    &amp;lt;html:submit property="dispatch=delete" value="刪除"/&amp;gt;&lt;br /&gt; * &amp;lt;/html:form&amp;gt;&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * 注意 property 裡面的值是 'dispatch=xxxx' 記得要寫等號與 method 名稱，&lt;br /&gt; * 而且大小寫要對，不能空白。當網頁按下 "儲存" 時，則會執行 /some/work.do &lt;br /&gt; *  的 save(...) 的 method。 如果按下 "刪除" 則執行 delete(...)，&lt;br /&gt; * &lt;br /&gt; * 建議 -- 這種寫法通常是用在一個 Action 有多個 dispatch method，而每個 method &lt;br /&gt; * 都共用同個 ActionForm&lt;br /&gt; * &lt;br /&gt; * (2) URL 法，接在 URL 後面：&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; *    &amp;lt;html:form action="/some/work?dispatch=save"&amp;gt;&lt;br /&gt; *     或是用 link 也可以&lt;br /&gt; *    &amp;lt;html:link action="/some/work?dispatch=save" /&amp;gt;&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * 建議 -- 通常用在不需要 ActionForm 的 Action，或者是要將 submit button 法&lt;br /&gt; *           寫成 url 時使用。&lt;br /&gt; * &lt;br /&gt; * (3) struts-config 法，直接寫死在 parameter='dispatch=foo' 上&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; *   &amp;lt;action &lt;br /&gt; *       path="/saveOrder" &lt;br /&gt; *       name="SaveOrderForm"&lt;br /&gt; *       type="antar.order.web.OrderDispatchAction"&lt;br /&gt; *       parameter="dispatch=save" &amp;gt;&lt;br /&gt; *   &amp;lt;/action&amp;gt;&lt;br /&gt; *   &amp;lt;action &lt;br /&gt; *       path="/deleteOrder" &lt;br /&gt; *       name="DeleteOrderForm"&lt;br /&gt; *       type="antar.order.web.OrderDispatchAction"&lt;br /&gt; *       parameter="dispatch=delete" &amp;gt;&lt;br /&gt; *   &amp;lt;/action&amp;gt;&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * 建議 -- 這種寫法通常是為了讓 Action 中每個 dispatch method 使用不同&lt;br /&gt; * 的 ActionForm。一旦寫死在 struts-config 裡，該 mapping 的 path 就不能&lt;br /&gt; * 與 URL 法 或是 submit button 法同時使用。&lt;br /&gt; * &lt;br /&gt; * 最後請注意同一個 request 上，URL 法不能與 submit button 法同時使用：&lt;br /&gt; * &lt;br /&gt; * &amp;lt;code&amp;gt;&lt;br /&gt; *  ...........錯誤範例...........&lt;br /&gt; * &amp;lt;html:form action="/some/work?dispatch=save"&amp;gt;&lt;br /&gt; *    .... some thing ....&lt;br /&gt; *    &amp;lt;html:submit property="dispatch=delete" value="刪除"/&amp;gt;&lt;br /&gt; * &amp;lt;/html:form&amp;gt;&lt;br /&gt; * &amp;lt;/code&amp;gt;&lt;br /&gt; * &lt;br /&gt; * @author ingram&lt;br /&gt; *  &lt;br /&gt; */&lt;br /&gt;public abstract class SimpleDispatchAction extends ActionSupport {&lt;br /&gt;&lt;br /&gt;    private static final String KEY_VALUE_SEPERATOR = "=";&lt;br /&gt;&lt;br /&gt;    private final static String KEY = "dispatch";&lt;br /&gt;&lt;br /&gt;    private static final String VALID_PARAMETER_NAME_PATTERN = KEY + "\\"&lt;br /&gt;            + KEY_VALUE_SEPERATOR + "[a-zA-Z0-9_]+";&lt;br /&gt;&lt;br /&gt;    private Class clazz = this.getClass();&lt;br /&gt;&lt;br /&gt;    private Class[] argTypes = new Class[] { ActionMapping.class,&lt;br /&gt;            ActionForm.class, HttpServletRequest.class,&lt;br /&gt;            HttpServletResponse.class };&lt;br /&gt;&lt;br /&gt;    private Map dispatchMethods = new HashMap();&lt;br /&gt;&lt;br /&gt;    public final ActionForward execute(ActionMapping mapping, &lt;br /&gt;            ActionForm form, HttpServletRequest request, &lt;br /&gt;            HttpServletResponse response) throws Exception {&lt;br /&gt;&lt;br /&gt;        String methodName = getMethodName(request, mapping);&lt;br /&gt;&lt;br /&gt;        Method method = null;&lt;br /&gt;        try {&lt;br /&gt;            Object[] args = { mapping, form, request, response };&lt;br /&gt;            method = obtainDispatchMethod(methodName);&lt;br /&gt;            return (ActionForward) method.invoke(this, args);&lt;br /&gt;        } catch (NoSuchMethodException e) {&lt;br /&gt;            throw dealWithMethodProblem(methodName, e);&lt;br /&gt;        } catch (IllegalAccessException e) {&lt;br /&gt;            throw dealWithMethodProblem(methodName, e);&lt;br /&gt;        } catch (InvocationTargetException e) {&lt;br /&gt;            throw dealWithMethodProblem(methodName, e);&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    private RuntimeException dealWithMethodProblem(String methodName,&lt;br /&gt;            Exception e) {&lt;br /&gt;        return new RuntimeException(&lt;br /&gt;                "can not access dispatching method:["&lt;br /&gt;                        + methodName&lt;br /&gt;                        + "]. ", e);&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    private Method obtainDispatchMethod(String methodName)&lt;br /&gt;            throws NoSuchMethodException {&lt;br /&gt;        Method method = (Method) dispatchMethods.get(methodName);&lt;br /&gt;        if (method == null) {&lt;br /&gt;            method = clazz.getMethod(methodName, argTypes);&lt;br /&gt;            dispatchMethods.put(methodName, method);&lt;br /&gt;        }&lt;br /&gt;        return method;&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    static String getMethodName(HttpServletRequest request,&lt;br /&gt;            ActionMapping mapping) {&lt;br /&gt;&lt;br /&gt;        final List gatherAllMethodNames = new ArrayList();&lt;br /&gt;        for (Enumeration e = request.getParameterNames(); &lt;br /&gt;               e.hasMoreElements();) {&lt;br /&gt;            String parameterName = (String) e.nextElement();&lt;br /&gt;            addMatchedParameter(gatherAllMethodNames, parameterName);&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        final String[] values = request.getParameterValues(KEY);&lt;br /&gt;        if (values != null) {&lt;br /&gt;            for (int i = 0; i &lt; values.length; i++) {&lt;br /&gt;                gatherAllMethodNames.add(values[i]);&lt;br /&gt;            }&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        if (mapping.getParameter() != null) {&lt;br /&gt;            addMatchedParameter(gatherAllMethodNames&lt;br /&gt;                           , mapping.getParameter());&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        if (gatherAllMethodNames.isEmpty()) {&lt;br /&gt;            throw new IllegalArgumentException(&lt;br /&gt;                    "no 'dispatch=methodName' found in parameter");&lt;br /&gt;        } else if (gatherAllMethodNames.size() &gt; 1) {&lt;br /&gt;            throw new IllegalArgumentException(&lt;br /&gt;                    "\nMultiple dispatch parameter: " + gatherAllMethodNames&lt;br /&gt;                            + " Only one parameter is allowed.");&lt;br /&gt;        } else {&lt;br /&gt;            return (String) gatherAllMethodNames.iterator().next();&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    private static void addMatchedParameter(List gatherAllMethodNames,&lt;br /&gt;            String parameterName) {&lt;br /&gt;        if (parameterName.matches(VALID_PARAMETER_NAME_PATTERN)) {&lt;br /&gt;            gatherAllMethodNames&lt;br /&gt;                    .add(parameterName.split(KEY_VALUE_SEPERATOR)[1]);&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;嘿嘿，有了這個統合的 Dispatch，日子就好過多啦！我也另外用同樣的邏輯寫了個 SimpleDispatchActionForm，搭配起來用不錯。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111431546963091310?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111431546963091310/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111431546963091310' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111431546963091310'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111431546963091310'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/simplied-struts-dispatchaction.html' title='Simplied Struts DispatchAction'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111365786664448356</id><published>2005-04-16T20:53:00.000+08:00</published><updated>2005-04-16T21:36:49.330+08:00</updated><title type='text'>靠！EasyMock</title><content type='html'>靠！真的想自殺，居然誤解了 EasyMock 這麼久！ &lt;br /&gt;長久以來，寫 mock 我都是一直在 test case 裡寫個 static inner mock class，供各個 test 使用。當初學這套方法時也調查了很多 tool，但是都沒有完美的 solution，當時的 easymock 只能套在 interface 上，完全不合用。而 Dynamock / jMock 族系的需要繼承特殊的 TestCase，而且他的 "錄製" 過程中呼叫 method 是用 method name 的，像這樣：&lt;pre&gt;&lt;br /&gt;mockSubscriber.expects(once()).method("receive").with( eq(message) );&lt;br /&gt;&lt;/pre&gt; &lt;br /&gt;天啊，Subscriber.receive() 的呼叫是用 String，這樣 refactor method name 時不就掛了！！而且真的很難看懂他在幹嘛。所以... 就打消使用 mock tool 的念頭了... &lt;br /&gt;昨個亂逛居然發現 easymock 已經有 extension 可以用在 class 上了！而且還是去年就有了！看看下面的這幾行標準的 easymock 使用：&lt;pre&gt;&lt;br /&gt;private MockControl control;&lt;br /&gt;&lt;br /&gt;private OrderDAO mockOrderDAO;&lt;br /&gt;&lt;br /&gt;protected void setUp() throws Exception {&lt;br /&gt;    //MockClassControl 可以替 class 建立 Mock&lt;br /&gt;    control = MockClassControl.createStrictControl(OrderDAO.class);&lt;br /&gt;    mockOrderDAO = (OrderDAO) control.getMock();&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;public void testSaveOrder() {&lt;br /&gt;    //開始預錄&lt;br /&gt;    Order order = new Order();&lt;br /&gt;    //執行真正的 method 來錄製，而不是用 method name&lt;br /&gt;    mockOrderDAO.saveOrder(order);  &lt;br /&gt;    control.replay();&lt;br /&gt;    //錄製完成    &lt;br /&gt;&lt;br /&gt;    OrderServiceImpl service = new OrderServiceImpl();&lt;br /&gt;    service.setOrderDAO(mockOrderDAO);&lt;br /&gt;    service.saveCustomerOrder(order);&lt;br /&gt;    //比對錄製結果&lt;br /&gt;    control.verify();&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;就是這麼簡單，唯一的限制是需要非 final 的 public constructor，不過這是小事啦！&lt;br /&gt;居然一直停留在 mock tool 限制很多的印象裡... 該死！&lt;br /&gt;&lt;br /&gt;靠~~~~&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111365786664448356?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111365786664448356/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111365786664448356' title='3 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111365786664448356'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111365786664448356'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/easymock.html' title='靠！EasyMock'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111247544514432977</id><published>2005-04-03T04:35:00.000+08:00</published><updated>2005-04-03T11:07:18.473+08:00</updated><title type='text'>為什麼 Mac 不好？</title><content type='html'>自從上個月買到了 mac mini 之後，這個月來總算圓了長久以來的 mac 夢，真是過足了癮。但是... 說真的，mac 真的只能當一台放在客廳的電腦，問題真不是普通的多...&lt;br /&gt;&lt;br /&gt;What I love Mac:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;UI 的反應速度不錯，效果也很多，整體的感覺非常的 smooth。這是 windows/linux 遠遠及不上的。&lt;/li&gt;&lt;li&gt;Dock 的概念很新，一開始用還不大能適應，因為程式關了視窗卻不是關掉程式本身，只能算是 Hide 而已。不過久而久之，你反而會習慣讓 OS 自己去管理開啟的程式，只要 RAM 夠大 就好 (個人是用 1g) 。完全不用去傷腦筋現在開了多少程式，多少視窗 (windows 的 task bar 就是這樣)&lt;/li&gt;&lt;li&gt;Eye candy 很多，很炫很酷。show 給大家看時很爽~~&lt;/li&gt;&lt;li&gt;字形 smooth 之後很漂亮。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;PDF anywhere，這點就真的很強了。唯有 pdf 才是真正的 portable 文件啊！&lt;/li&gt;&lt;li&gt;好用的 expose ，視窗滑來滑去的好看，找起來也快，設定好之後用滑鼠就能操作，讚。&lt;/li&gt;&lt;li&gt;支援 MS Office, Oracle 9i/10g, Eclipse, Firefox, msn... 等等我每天必用的軟體。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Unix based, built-in XWindow, open ldap, and cvs server... etc&lt;/li&gt;&lt;li&gt;支援 palm 的同步，光這個就打死 linux 了。&lt;/li&gt;&lt;li&gt;系統設定很簡單 (網路、硬體.... etc)&lt;/li&gt;&lt;li&gt;有免費的 remote desktop control 可用，有這個真的很方便。&lt;/li&gt;&lt;li&gt;Virus/Spyware free !&lt;br /&gt;&lt;/li&gt;&lt;li&gt;Mac mini: 超小台，可以當 notebook 用了。個人每天帶著這台上下班喔。&lt;/li&gt;&lt;/ul&gt;What I hate Mac:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;中文，中文！！支援中文實在太爛了。&lt;/li&gt;&lt;li&gt;首先 iTune 遇到非unicode 的 id3 tag 中文通通掛掉。是啊，網路上是有轉碼的程式可用，不過只要轉過一次就知道，掉字的掉字，亂碼的亂碼，還是不可行，而且只能轉 mp3，不能轉 mpc/ogg。&lt;/li&gt;&lt;li&gt;第 二，從 windows office 來的檔案打開後中文常常變亂碼。大概是 windows 和 OS X 處理中英字型混用的方式不大相同造成的，也有可能是 office 自己搞爛的。還有就是 office 2004 和一般 windows office 相容性很爛，常常排版都亂掉了，這當然是 MS 的錯 (而且錯的愚蠢) 。但不管是誰造成的，在 Mac 上就是別想好好用 office 了。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;第三，中文字 Apple LiGothic 的英文字太小，但偏偏是系統字型，改都不能改，可憐的 台灣 Mac 族長久以來一直不斷忍受...  直到現在，大家還是在忍受...&lt;/li&gt;&lt;li&gt;第四，輸入中文時，有時候第一個字的英文，有時候會是中文，跳來跳去....  很爛。&lt;/li&gt;&lt;li&gt;第五，palm 是支援了，不過中文上還是有些小問題。而且 palm 已經宣佈不在支援 Mac 了，真糟糕。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;keyboard binding 與 PC 系統相差太大。PC 都是以 ctrl 鍵當作指令的起點，而 Mac 則是 command 鍵，每個系統 keybinding 不一樣是理所當然... 不過當 Mac 在網頁上時就吃虧了。有些網頁可以用 hotkey 操作地，不過大多是設計給 PC 用的，這時就不相容啦。另外 unix 下的軟體也都是用 ctrl 操作的 (unix 可沒有 command key) 所以如果一旦同時操作 Unix terminal 和其他 OS X 原生軟體，你會發現一會要用 ctrl，一會要用 command，手忙腳亂，效率大減。&lt;br /&gt;&lt;/li&gt;&lt;li&gt;java，java !! Eclipse 實在太慢了，eclipse 的反應速度跟不上我寫程式的節奏，寫程式變的一頓一頓的，超不爽！&lt;/li&gt;&lt;li&gt;Firefox，Firefox !! Firefox 慢死了，受不了的慢~~~&lt;br /&gt;&lt;/li&gt;&lt;li&gt;JDK 5.0 ! jdk 5.0 已經出半年了，到現在還沒有影子，不要跟我說那個什麼 Tiger preview，當其他 OS 已經有免費的可下載了，誰還要花一堆精神去搞不大能用的半成品 ?而且還非 OSX 10.4 才可用，可笑可笑。&lt;/li&gt;&lt;li&gt;雖 然是 Unix based 的系統，當你要裝 unix 的東東時... 比如 open ldap, cvs server... 時就會發現其實不能照 linux 的方式來裝，有很多地方要改，這也是理所當然，每個 OS 都有自己的設定方式。但是重點是資源少的可憐，甚至是沒有。而且 OSX 還綁了一個 netinfo，搞的都跟大家不一樣，找不到資料時就只好放棄... 。如果花了很多時間學習 OSX 上的 unix，投資報酬率很低的，因為好不容易學了一堆東西，你不見得有地方可以發揮，對工作上幫助並不大 (台灣有幾家公司用 OS X server ??)&lt;/li&gt;&lt;li&gt;free 的軟體很多，但是完成度都不是很高，有哪個 Mac 軟體能像 foobar2000 一樣通吃所有格式，超強的 mass tag，外加極佳的音質？ 有哪個 Mac 軟體能像 filezilla 一樣提供全功能的 ftp client，可以 queue file transfer？有哪個 Mac 軟體能像 xnview 一樣超快速的瀏覽圖片外加 batch 修改圖的功能？有哪個 Mac 軟體可以像 7-zip 一樣通吃所有格式，提供方便的 UI ? (7-zip windows 的 UI 並不怎麼樣，但是 Mac 的更不像話！)&lt;/li&gt;&lt;li&gt;Eye candy 是不錯啦... 不過看久了 (我用了一個月) 就覺得沒什麼了，還是效率比較重要。&lt;/li&gt;&lt;li&gt;Mac 的東西貴，太貴了。我現在買的是 apple 大放送的 mac mini，如果萬一未來被 Mac 綁死了，就只好繼續買 Mac，一台像樣的 powerbook 多少錢？ 9萬元！天啊~~~&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;大致上來說，只要有軟體跨平台跨到 Mac ，通常在 Mac 的效率都是最差的，偏偏這些跨平台的軟體都是公認最好用的，例如 firefox, eclipse (軟體可以跨平台跨到 Mac 表示極受大家愛用)。我想了一想，效率差原因可能是：&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Mac 上程式不好寫 or 不好最佳化&lt;/li&gt;&lt;li&gt;Mac 的 (好) developer 少&lt;/li&gt;&lt;li&gt;Mac 的硬體太慢&lt;/li&gt;&lt;li&gt;那些程式一開始就不是在 Mac 上寫的，是 port 過去的&lt;br /&gt;&lt;/li&gt;&lt;/ul&gt;第 一點比較難判斷，而 firefox 和 eclipse 慢的原因應該是第四點吧... 不過第二、第三點也不無可能。Mac 用的人少，自然吸引不了什麼開發者，很多公司後來也只出 windows 版的軟體了。Mac 的硬體慢嗎？也許某些領域 Mac 不錯，不過就 notebook (mac mini 算是歸在 notebook 內) 來講 Mac 已經落後 PC 太多。PC 在 intel/AMD 兩家的競爭嘶殺之下，進步快多了。而所謂的 G5 powerbook 也還生不出來，就算生出來了一定是爆貴 (台幣 9~10萬 吧，誰買的起？) 而且也不見得比 PC based 的 notebook 快。&lt;br /&gt;&lt;br /&gt;哎....  看來還是得回 Windows... 沒錢玩不起高貴的 Mac&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111247544514432977?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111247544514432977/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111247544514432977' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111247544514432977'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111247544514432977'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/mac.html' title='為什麼 Mac 不好？'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111246421016601412</id><published>2005-04-03T01:42:00.000+08:00</published><updated>2005-04-03T01:50:10.166+08:00</updated><title type='text'>Eclipse tips on source code</title><content type='html'>Try to attach source codes for *.jar, then the auto-generated source (such as method) will comes with meaningful name, for example, A generated execute(...) method of Struts Action:&lt;br /&gt;&lt;br /&gt;without attach source:&lt;br /&gt;&lt;pre&gt;    public ActionForward execute(&lt;br /&gt;           ActionMapping arg0,&lt;br /&gt;           ActionForm arg1,&lt;br /&gt;           HttpServletRequest arg2,&lt;br /&gt;           HttpServletResponse arg3&lt;br /&gt;           throws Exception {&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;lots of ugly arg0, arg1.... !!!&lt;br /&gt;&lt;br /&gt;after attach Struts source codes to struts.jar:&lt;br /&gt;&lt;pre&gt;    public ActionForward execute(&lt;br /&gt;           ActionMapping mapping,&lt;br /&gt;           ActionForm form,&lt;br /&gt;           HttpServletRequest request,&lt;br /&gt;           HttpServletResponse response)&lt;br /&gt;           throws Exception {&lt;br /&gt;   }&lt;br /&gt;&lt;/pre&gt;whew! much better !&lt;br /&gt;&lt;br /&gt;Another tip is when you attach source codes, you can view java docs without assigning javadoc location. Eclipse will parse javadocs that inside attached source codes for you.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111246421016601412?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111246421016601412/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111246421016601412' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111246421016601412'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111246421016601412'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/04/eclipse-tips-on-source-code.html' title='Eclipse tips on source code'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-111129472360277863</id><published>2005-03-20T12:54:00.000+08:00</published><updated>2005-03-20T13:01:27.950+08:00</updated><title type='text'>Steam Boy</title><content type='html'>Steam boy 是建構在 "空想科學" 上的作品，空想科學就是用現在已知的科學去推演、幻想可能的科學產物，最有名的就是那些日本機器人動畫了。本片中不時會看到什麼壓力太小，閥門開開關關之類的動作，這些道理都是日常生活中可以理解的科學，只不過現實中不會造出這麼大的蒸氣城 (就嚴謹的科學來講也是做不出來的)。在日本到是有很多人迷空想科學的作品地，通常越寫實他們看的越是起勁，就像迷科幻小說那樣。相信他們看到這齣作品會痛苦流涕、感動不已吧...&lt;br /&gt;&lt;br /&gt;就技術面來講，這片是無懈可擊了，可惜劇情上有點托的感覺... 不斷的重覆科學帶來的負面影響，兩個小時下來真的會覺得厭煩....&lt;br /&gt;&lt;br /&gt;撇開男主角不提 (成長中的熱血少年... 沒新意) 那鍋女主角讓我嘖嘖稱奇。性格膽小、驕傲、大小姐脾氣、不懂事世... etc 缺點說不完。她有一幕很經典，就是她的管家說要跟英國開打了，她的回答居然是 "不淮輸" ! 真是讓人覺得又氣又好笑。跟男主角的互動也有不少有趣的橋段。跟宮崎駿的作品相比之下，差最多的就是她了，宮崎駿的作品的主角都是女性，不管有多少挫折，在荒亂的冒險中都會發揮女性特有的母性、而且勇敢去擁抱所愛。這樣的女性就像聖女一般令人可敬可佩，不過就是太夢幻了點...   可算得上是 "空想少女" 了。Steam boy 女主角就真實的多了。&lt;br /&gt;&lt;br /&gt;Howl's Moving Castle 和 Steam Boy 各有優缺點，其實不該拿來相比的。不過若要說誰比較好看，我還是比較喜歡 Howl。&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-111129472360277863?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/111129472360277863/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=111129472360277863' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111129472360277863'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/111129472360277863'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/03/steam-boy.html' title='Steam Boy'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110965323480310372</id><published>2005-03-01T12:57:00.000+08:00</published><updated>2005-03-01T13:01:55.413+08:00</updated><title type='text'>No, I will not fix your computer.</title><content type='html'>不要再做好人囉：&lt;a href="http://epaper.pchome.com.tw/archive/last.htm?s_date=old&amp;s_dir=20050228&amp;amp;s_code=0153&amp;amp;s_cat="&gt;奇文共賞&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;看完之後，腦袋直覺的跑出一句諺語：&lt;br /&gt;&lt;blockquote&gt;No, I will not fix your computer.&lt;/blockquote&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110965323480310372?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110965323480310372/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110965323480310372' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110965323480310372'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110965323480310372'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/03/no-i-will-not-fix-your-computer.html' title='No, I will not fix your computer.'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110949046190662779</id><published>2005-02-27T15:26:00.000+08:00</published><updated>2005-02-27T19:22:22.396+08:00</updated><title type='text'>Music from Howl Moving Castle: Merrygo-round of Life</title><content type='html'>雖說這幾天玩 mac mini 玩到瘋了，哎… mac 實在另人又愛又恨啊，說來話長啊…&lt;br /&gt;先不談這個，今個兒拿到了霍爾的 OST (Original SoundTrack)&lt;br /&gt;&lt;br /&gt;  &lt;span style="font-size:180%;"&gt;！&lt;/span&gt;&lt;span style="font-size:180%;"&gt;！&lt;/span&gt;&lt;span style="font-size:180%;"&gt;！&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;真是好聽呀。opening 之後的 &lt;span style="font-weight: bold;"&gt;空中散步&lt;/span&gt; 圓舞曲，聽了之後腦中馬上回憶起蘇菲和霍爾在園遊會的上空漫遊的場景。這一幕配上這個音樂真是讚 (想不出啥好詞…) 隨後的是 &lt;span style="font-weight: bold;"&gt;さすらいのソフィー&lt;/span&gt; (流浪的蘇菲)，這一段比較長，利用主題曲的不斷變奏來描寫流浪的各種情境。大概是一分半的左右吧，有一段不知是用什麼笛吹的，略帶著滄桑之意，這一段我 也很喜歡。&lt;span style="font-weight: bold;"&gt;サリマンの魔法陣～城への帰還&lt;/span&gt; 這一首前半是沙利曼的光魔法，那個像是異族的聲音聽來很詭異。後半則是坐飛機逃走，很有天空之城之影子 (誰叫它飛機要設定的這麼像咧 ？)&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;花園 &lt;/span&gt;這一首帶哀傷的氣氛，卻充滿了情感的張力，有點欲言又止的那種感覺…(我到底在說啥？) 後面的 &lt;span style="font-weight: bold;"&gt;戰火の戀&lt;/span&gt; 也有一部份跟這首很相似，不過戰火那段帶來的感受卻大不相同，比較像是充滿歷經苦難，終於開花結果的感覺。&lt;br /&gt;&lt;br /&gt;除了以主旋律為主的音樂之外，其他的多半都是乒乒乓乓的打仗、奔走等等配樂，不適合單獨聽啦…  片頭和片尾都有一段叫 &lt;span style="font-weight: bold;"&gt;人生のメリーゴーランド&lt;/span&gt;  (人生的 merrygo-round，旋轉木馬) 我想這就是主題旋律的名稱吧。啊，只要不放棄人生都還是大可轉圜的。&lt;br /&gt;&lt;br /&gt;總之很好聽囉，這陣子不愁沒音樂聽了！推薦給大家~~&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110949046190662779?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110949046190662779/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110949046190662779' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110949046190662779'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110949046190662779'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/music-from-howl-moving-castle-merrygo.html' title='Music from Howl Moving Castle: Merrygo-round of Life'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110908624468713821</id><published>2005-02-22T22:33:00.000+08:00</published><updated>2005-02-22T23:30:44.690+08:00</updated><title type='text'>Spring Training</title><content type='html'>當然，這不是補習班的 Training... 業界是不會有 open source 技術的 training course 的。過去我們這個 team 在採用技術時，都是先請顧問來教個一兩天 (Struts &amp; Hibernate) 然後我們就開始自己 Coding 。而這一次要導入的是 Spring Framework，原因是…&lt;br /&gt;&lt;blockquote&gt;我自己想用！&lt;/blockquote&gt;前先日子花了很多工夫鑽研 Spring，發現真是棒啊！真的很想在我們的專案裡導入。可是呢，礙於其他人的對 Spring 的熟悉狀況不一，而且採用 spring 一開始並不會有立竿見影的效果，想要導入還真的不容易。為此，只好自己下海，充當起臨時教練。拿起現有所有可得的書東抄西湊的，總算弄了個 50 張投影片，這50張還只是 cover 一半而已 (spring 東西還真多)。今個兒就花了一整天跟組員介紹 Spring 的基礎，以及 persistence 相關的主題。另外，也設計了幾個小 LAB 讓大家練習練習。&lt;br /&gt;&lt;br /&gt;事實證明，真的要讓大家做做 lab 才有用。有些人吸收很快，三兩下lab 就做完了。有些人則很明顯，我講的東西他大概吸收很有限… 一做 lab 就發現問題層出不窮。遇到這種情形，我真擔心導入 Spring 對我們 team 會不會太早或太苛求？個人是個 Java geek，就是那種很喜歡玩新的  或是自個兒打造 Framework 的那種人，玩這種東西比吃飯還簡單。但對於接觸不深的人就很麻煩了，要導入新技術，就要花很多時間拉拔這些人…  之前還未做 Training 時我就考慮很多這方面的問題，不過到是沒想到，TEAM 裡的技術之差異居然有這麼大。&lt;br /&gt;&lt;br /&gt;念及至此，我反到覺得更要導入 spring 了，對那些 junior 的 programmer，更要強迫他們使用 spring。原因是 spring 提供許多便捷的工具，以及更不易出錯的架構。雖然那些不熟的人一開始可能不曉得為什麼要這樣做/那樣做。但最少，他們寫出來的，整體來講問題會比較少。像今天講課提到的 jdbcTemplate 就是個很好的例子。與其放任他們寫 free 的 jdbc code，倒不如要求他們用 jdbcTemplate，雖然一開始學會比較慢，但長期來看問題反到會比較少。&lt;br /&gt;&lt;br /&gt;個人認為，除非 server-side java 有新的特別突破，我相信 Spring 應該是我們 team 導入的最後一個 Framework 了。剩下的，就是一直換新版這樣 (如 Hibernate2 -&gt; Hibernate3... etc)。不單單是因為 Struts + Hibernate + Spring 已足夠應付開發的需求，也是因為 "玩"  framework也該有個限度 (雖然我很喜歡玩...)。就技術層面來看，Team 裡(包括我自個兒) 下一個階段比較需要加強的是 domain 的設計及 performance 的 tuning。&lt;br /&gt;　&lt;br /&gt;　&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110908624468713821?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110908624468713821/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110908624468713821' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110908624468713821'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110908624468713821'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/spring-training.html' title='Spring Training'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110889421408738230</id><published>2005-02-20T18:05:00.000+08:00</published><updated>2005-02-20T18:18:18.926+08:00</updated><title type='text'>From NX70 to Treo650</title><content type='html'>終於拿到 Treo650 囉，不過跟 NX70 一比.. 怪怪：&lt;br /&gt;&lt;br /&gt;怎麼沒有像 MS Import 的工具？&lt;br /&gt;也沒有 Backup 的 tool...&lt;br /&gt;鬧鐘的功能有跟沒有一樣... 只能設時間不能設日期...&lt;br /&gt;而且連最基本的 File Manager 都沒有！太扯了！&lt;br /&gt;&lt;br /&gt;也許 palm 本來就是這個樣子吧... 現在才知道 CLIE 的好啊~~&lt;br /&gt;&lt;br /&gt;anyway, 找了很多軟體：&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;Card Export II 2.2&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;將 SD 卡 Export 成一個 USB 硬碟，安裝完後就直接可以用了，windows XP 不需要額外的 driver。速度不錯，我 copy 了 1G 的 mp3 沒有出錯，相當穩定。&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;FileZ 6.5&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;隨便找來的一個 file manager，還不錯用。可拿來硬殺一些其他 file manager 殺不掉的檔案&lt;br /&gt;&lt;ul style="font-weight: bold;"&gt;&lt;li&gt;zlauncher 5.10.3&lt;/li&gt;&lt;/ul&gt;以前我在 NX70上都是用 Launcher X 的.... 後來聽說作者消逝了... 只好換別的了，zlauncher功能相當強大，而且有內建 File manager，也支援 Treo (上面有圖示顯示手機收訊狀況) SD card 插入時也會自己跑出一個 file tab 出來，很方便。缺點是：超醜！醜斃了！雖然有很多 theme 可以選，但是個人對那種審美觀... 不敢領教。而且很多地方看的出歷史的鑿痕... 在舊的 low resolution UI 上再加上新的 high reolution 的 UI，新舊穿插很不協調...&lt;br /&gt;&lt;ul style="font-weight: bold;"&gt;&lt;li&gt;CJKOS 4.62 (港版附光碟)&lt;/li&gt;&lt;/ul&gt;只有一個字，醜..........&lt;br /&gt;我要掌龍啦~~ 掌龍有沒有計畫要支援 Treo ? 還是已經....&lt;br /&gt;anyway... 以上只是就字型的 complain.... 使用上還不錯啦，撘配前輩的 Unicode 補完計畫&lt;br /&gt;+ 草蝦輸入法後，真的是如魚得水。他的 cManager 可以將字形放在記憶卡上，我只放 16x16 的在內建記憶體裡。不過令我驚訝的是即使是放在卡上，用起來也很快 (完全沒感覺有 delay) (Treo650+CJKOS+ATD 66x 1G SD card) 讀字型遠快於 (NX70 + 掌龍 + MS card 128M)，於&lt;br /&gt;是誰貢獻的多，就不清楚了。&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;PowerRun 1.3&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;將程式與資料一併放到記憶卡的好東西，這就不多提了。1.3版在 Treo650 上工作正常無誤。&lt;br /&gt;&lt;ul style="font-weight: bold;"&gt;&lt;li&gt;Agendus pro 9.02&lt;/li&gt;&lt;/ul&gt;個人愛用的 PIM 強化工具，支援點 contact 直接 dial。可惜有時後不曉得為什麼會突然 soft reset (還好不常發生)&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;Textplus 5.6&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;5.6 版正式支援 Treo650。這是英文 type saving 的工具，就是打了 te 它旁邊會出現一些相關的字詞 (如text, test... etc) 讓你用 5-way navigator 直接選。&lt;br /&gt;&lt;ul style="font-weight: bold;"&gt;&lt;li&gt;Palmary Clock 3.02&lt;/li&gt;&lt;/ul&gt;palm 本身附的 world clock 不敷使用。找來找去，先找到這個頂著先。有附碼錶，鬧鐘也可以依禮拜幾來設定，也可以與 Aeroplay 或 pocketTunes 搭配，改用 mp3 當作鬧鈴聲(不過我用不到啦....) 只可惜太大了，足足 1MB。因為鬧鐘需要常駐，所以不能放在卡上...只是為了個鬧鐘就花了 1MB 真是太浪費了...&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;pocket tunes deluxe 3.0.6&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;mp3/ogg player, deluxe 版多支援 wma。買 treo650 不單單只是想要兩槍合一，還要達到三槍合一 (music player+手機+pda)，這樣以後一付耳機，一台 Treo，就什麼都打發了。乾淨俐落！ 1G 的卡放 mp3 真的是夠聽了，( 我用的是 ADT 66x )&lt;br /&gt;ok, 回到主題，這個 player 還不錯用，加上Unicode補完計畫後，中日文都可正常顯示。也支援 Treo650 的 volume 控制 (左側邊的手機音量控制鍵)，我大多數的音樂都是 ogg 的，跑起來也都正常，不會有什麼 delay。音質的話受限於 Treo650 硬體的限制... 所以... 不過，它提供 5 格的 Equalizer，還有 bass boost。equlizer 是建議一定要調，稍補硬體的不足。但是它的bass boost 就免了，破音連連，勸大家還是不要用吧...其他特異功能像是接電話/掛電話後都會直接回覆到剛播放的曲子，很方便。更扯的是 hot sync 時音樂也不會中斷，雖然聲音會一頓一頓的，但總比 hot sync 完還要去開一次來的方便許多。&lt;br /&gt;&lt;ul style="font-weight: bold;"&gt;&lt;li&gt;Aeroplayer 5.1.1&lt;/li&gt;&lt;/ul&gt;也是 mp3/ogg player，中日文一樣 ok。不過不像 pocket tunes 一樣支援 treo 的手機音量鍵。它也提供 5格的 equalizer和 base boost，特別的是它 base boost 可以調 level，而且完全不會破音，而且聲音聽起來飽滿了許多，大大的補足硬體的不足啊！讚！可惜的是它不像pocket tunes 一樣... 它 hot sync 時音樂會斷，而且 base boost 吃 CPU 似乎也很大，放在背景聽，前景程式會變慢許多，音樂也會開始頓頓的。(聽音樂 + 玩 Bejeweled 明顯比較慢) 另外，它一啟動時，每次都會去掃 card 上的 mp3，而且很久，頗浪費時間。&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;span style="font-weight: bold;"&gt;realplayer&lt;/span&gt;... &lt;/li&gt;&lt;/ul&gt;這是啥？可以吃嗎？&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;音質不負責任主觀比較 (耳機為 Etymotic ER-4P + 隨便買的2.5-&gt;3.5轉接頭 )&lt;br /&gt;&lt;br /&gt;　　　　　iAudio M3　　　NX70　　　　　Tre650　　　　　　　Treo650&lt;br /&gt;software　　--　　　　　built in player　　pockettunes　　　　　Aeroplayer&lt;br /&gt;tuning　　(enable BBE)　(bass level 1)　　(enhance bass by eq)　(enable bass boost)&lt;br /&gt;&lt;hr /&gt;飽滿　　　　10　　　　　　　9　　　　　　8　　　　　　　　　8.5&lt;br /&gt;清昕　　　　10　　　　　　　9　　　　　　8　　　　　　　　　8.5&lt;br /&gt;音場　　　　10　　　　　　　9　　　　　　8　　　　　　　　　8&lt;br /&gt;&lt;br /&gt;註：&lt;br /&gt;&lt;ol&gt;&lt;li&gt;ER-4P 本來的 bass 就比較薄一點，所以個人偏好加一點 bass...&lt;/li&gt;&lt;li&gt;視 iAduio M3 為滿分的前提下比較&lt;/li&gt;&lt;li&gt;不論是 pocket tunes 或 Aeroplayer，如果不加強一點 eq 或是用 bass boost音樂都乾澀難以入耳....&lt;/li&gt;&lt;li&gt;除了 NX70 用 256K mp3 外，其於都是 ogg&lt;/li&gt;&lt;/ol&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110889421408738230?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110889421408738230/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110889421408738230' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110889421408738230'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110889421408738230'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/from-nx70-to-treo650.html' title='From NX70 to Treo650'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110866630706337435</id><published>2005-02-18T01:48:00.000+08:00</published><updated>2005-02-18T03:09:53.396+08:00</updated><title type='text'>ハウルの動く城 之我見</title><content type='html'>今個兒終於跟同事去看霍爾了，看完的第一個反應就是 惑爾 啊~ 真的是大大的不懂。原本還以為會有很感人的橋段，結果其實也還好嘛… (為此還被同事譏笑~~ )。不過回來的路上到是好好重新想了數遍... 這一齣戲的主軸，與其說是愛情故事，或是變幻莫測的魔法，我反到認為是 "內心的反映" (一時想不出好詞…)&lt;br /&gt;&lt;br /&gt;整齣戲最讓人疑惑、驚喜的便是主人公 蘇菲 一會兒年輕一會兒變老。一開始蘇菲被詛咒時，頓時變得非常年老，90歲的外貌便像是蘇菲當時的內心：處在不確定、退縮、安於現狀的心態 (她不肯面對自己不想繼承家業，不想做帽子，而且一直對自己的外貌沒信心)。當後來她在莎利曼面前，義無反顧的直言她了解、愛護霍爾，她的外貌一瞬間回覆到原來最年輕的樣子，此刻她的外表正反應她內心充滿著勇氣、愛的樣子。後來有很多橋段都可看出，當她願意放開心胸，鼓起勇氣時，外貌便越來越年輕 (像是到花園，或是霍爾替她做了她老家的臥室)；又像是她無法接受霍爾對她的讚美時，頓時便從年輕變為老婆婆... 這些一連串的變化，我想這是宮崎駿想要表達一個簡單的道理：當心深鎖，逃避時，你就像個很老的人一般，病厭厭的；而當打開心胸，勇敢擁抱時，你便整個人年輕了起來，神采飛揚。你的內心才是你真正的外貌。&lt;br /&gt;&lt;br /&gt;說到最令我印象深刻的橋段，是當霍爾自卑於髮色變深，身體變成一沱綠泥時… 蘇菲彷彿看到了她自己一樣... 終於忍不住在大雨中痛哭... 這一段我覺得最另人動容。其他什麼戰爭、魔法的，我到覺得都還是其次… &lt;br /&gt;&lt;br /&gt;宮崎駿的電影向來都會有關懷社會的一面，像是風之谷講的是環保，神隱少女則是諷刺現代人的暴食浪費，而霍爾則有人引申為現代的社會已經是高齡化的社會，鼓勵銀髮族 "人老心不老"，勇於開創第二春。(不過我覺得這樣轉太硬了…) &lt;br /&gt;&lt;br /&gt;總之... 霍爾算是不錯的電影啦，可是遠遠不及神隱少女當初帶給我的驚奇、感傷及快樂。建議有興趣的人到時可以租個 DVD 看看。不過，到電影院則免了吧。&lt;br /&gt;　&lt;br /&gt;　&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110866630706337435?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110866630706337435/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110866630706337435' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110866630706337435'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110866630706337435'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/blog-post_18.html' title='ハウルの動く城 之我見'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110838876870449387</id><published>2005-02-14T21:30:00.000+08:00</published><updated>2005-02-14T21:46:08.706+08:00</updated><title type='text'>手又癢了，又給它錄了一首 SRW OG2</title><content type='html'>本來以為很久沒電動，本想對電動音樂的熱情該差不多熄了... 這一次玩了 OG2 後，那個ラトゥーニ的音樂真是讚啊 (雖然只是 GBA 音源)，又給忍不住錄下去了。看來這種錄 BGM 的習慣是改不了：&lt;br /&gt;&lt;br /&gt;    &lt;a href="http://ingram.myweb.hinet.net/17_Fairy_Dang-Sing.mp3.bin"&gt;17 Fairy Dang-Sing.mp3&lt;/a&gt;  按右鍵另存新檔後，將副檔名的 .bin 去掉即可&lt;br /&gt;&lt;br /&gt;好聽好聽~~&lt;br /&gt;　&lt;br /&gt;　&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110838876870449387?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110838876870449387/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110838876870449387' title='3 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110838876870449387'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110838876870449387'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/srw-og2.html' title='手又癢了，又給它錄了一首 SRW OG2'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110836056622155109</id><published>2005-02-14T13:53:00.000+08:00</published><updated>2005-02-14T14:57:29.173+08:00</updated><title type='text'>測試一下圖片功能</title><content type='html'>剛註冊了 flickr ，這玩意還真是漂亮啊~&lt;br /&gt;&lt;br /&gt;&lt;iframe height='630' width='533' src="http://www.flickr.com/photos/xexex/sets/120058/show/" &gt; &lt;/iframe&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;Normal Size:&lt;br /&gt;&amp;lt;iframe height='630' width='720'&lt;br /&gt;      src="http://www.flickr.com/photos/xexex/sets/120058/show/" &amp;gt;&lt;br /&gt;&lt;br /&gt;Cutted Size:&lt;br /&gt;&amp;lt;iframe height='630' width='533'&lt;br /&gt;      src="http://www.flickr.com/photos/xexex/sets/120058/show/" &amp;gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;圖片裡是我最近想敗的 Treo 650 配件 (啊 Treo 650 還沒入手就先想配件，真是太敗家囉)。可惜的是 TreoCentral 沒有賣到台灣... 氣死了 !&lt;br /&gt;　　&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110836056622155109?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110836056622155109/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110836056622155109' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110836056622155109'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110836056622155109'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/blog-post.html' title='測試一下圖片功能'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-110822125880859258</id><published>2005-02-12T22:29:00.000+08:00</published><updated>2005-02-13T02:08:59.743+08:00</updated><title type='text'>好久沒寫啦，一堆 Spring 雜感</title><content type='html'>真是有夠久沒寫~~  現在想想大概是因為後來的文章越寫越大篇，料雖然很多，但是反而變成寫BLOG 的阻力...  下次該寫一點點就好囉。&lt;br /&gt;&lt;br /&gt;最近發生很多事：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;我 們的系統上線了~ 大概已經過了一個月吧，這個系統開發時導入 Unit Test 與 Continuous Integration。目前累積了大概 1500 個 unit test 吧，改需求/新增功能 都難不倒我們。 team 的人都說還好有 Test 這個 "安全網" ，不然喔上線後，不僅改功能無望，搞不好還得加班熬夜修東修西的。當初我嘗到 test 的甜頭後，便在 team 裡面不斷的 "叫囂" 說要大家寫 test ，現在總算是開花結果了。(個人用了很多嚴重的字眼逼大家寫… 還好結果不錯，不然我會被公X到死~~)&lt;/li&gt;   &lt;li&gt;1500 個 unit test 要 run 多久？ 加上 database 的 rebuild 大概要 30~40 分鐘。這真的太久囉。原因是我們用 ObjectMother 太多了，這個 ObjectMother 大多跟 DB (hibernate) 綁在一起，所以花了很多無謂的時間在 load hibernate configuration/insert/delete data上 ( ObjectMother with Hibernate 這個用起來太方便了，導致很多 test 都扯到 DB )。下個專案要多多改進 -- DB layer 還是要盡可能的 decouple from business logic啊&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;bug 的數量與TestCase的數量成反比！還有什麼比這個更有說服力呢？ Unit Test Rules !&lt;br /&gt;&lt;/li&gt;  &lt;/ul&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;oxoxoxoxoxoxoxo Spring 分隔線 xoxoxoxoxoxoxoxoxo&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;這 個系統後來我導入了 Spring... (擅自導入到我負責的 module...)。當初因為它又需要額外的 xml 檔，使人望之卻步 (refactoring 的大敵 !) 嘗試用了一個月，只能說真是相見恨晚啊。Spring 真的是 J2EE 開發者的天堂。Spring 的 IoC 只是個吸引人的點子 (別家的用起來反而比較簡單) 真正誘人的是它幾乎整合了所有 J2EE 開發的所有工具：新增 (如 declarative Transaction)、修正 (如 HibernateException 變 unchecked)、簡化 (如 quautz scheduler)、超輕量 (一個 container 居然只要幾個 xml 檔，真是太扯囉)、潛力無窮 (AOP)… etc 太強啦：它可不只是超級萬能瑞士刀，它是小叮噹的四次元口袋！拿得出一卡車的工具且還比瑞士刀輕 !! 哇靠這種好康上哪找啊！&lt;br /&gt;&lt;br /&gt;&lt;blockquote&gt;Spring + Hibernate == 現在寫程式的人真幸福&lt;br /&gt;&lt;/blockquote&gt;&lt;br /&gt;把 Spring 拿去跟 picocontainer / hivemind 去比的簡直是頭殼壞掉，這就像是拿 tomcat 和 oracle db 比一樣荒謬。&lt;br /&gt;&lt;br /&gt;現 在還沒嘗試的還有 Spring web，不過好不容易我們的 team 都已經搞清楚 Struts 的脾氣了 (真的是愛恨交加)。如果真要再花半年 Train Spring web，寧可勸大家改學 Tapestry 還比較划算。啊~~~ 真是懷念以前寫 Tapestry 的時光啊。anyway 來看看最近 K 的書吧：&lt;br /&gt;&lt;br /&gt;Spring 書推薦：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;J2EE without EJB (不是全名，請自行查 google) 這本是founder Rod Johnson 自個兒寫的，講了好多大道理啊。前半本大說 EJB 的不是，舉證例例，我是看了一點就狂睡了，基本上嘗過 EJB 的苦頭，就不必再多花時間囉，可直接跳到後半看 Spring。這本是講 Spring 的原理最詳細的一本，時間夠的話，這本是第二本必看的書。&lt;/li&gt;   &lt;li&gt;Spring in Action， in Action 系列再下一成，這本是Spring 書中最簡單的一本，推薦入門者先看這一本。用 Spring 該具備的觀念都有提到，Spring 所有重要的功能都有涉獵。我常常就直接拿他的範例 code 給它這麼一 copy/past，就直接可用了。(其實也是因為 Spring的設定太簡單囉)&lt;/li&gt;   &lt;li&gt;Spring Live (Matt Raible 著) appfuse 的開發者。這本是 source beat 的電子書 (只出電子版) ，更特別的是出版後一年之內作者還會追加章節，現在出到第11章了。這本是最 practical 的一本。帶你用TDD 的方式使用 Spring，而且提供 Spring整合其他 framework 的各種作法 (超級多)，還有很多實用的技巧。作者本身也是邊學邊寫完這一本的，所以很多大家會遇到的問題他都先碰到了。老實說作者原意是將這本寫成入門書的，其實定 位也是啦，只是他用的概念/framework 實在太多太廣 (光是 TDD 的開發方式就會讓有些人進不了門) 我覺得還是要在 java 界混久一點的人才適合看，作者的 blog 還蠻受歡迎，大家有空可以去瞧瞧。 &lt;/li&gt;   &lt;li&gt;Pro Spring... 剛出… 還沒入手… 有800頁厚&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;&lt;span class="sans"&gt;Professional Java Development with the Spring Framework  還沒出，不過是 Rod 那群人寫的，這一定是要入手的啦&lt;/span&gt;&lt;/li&gt;   &lt;li&gt;Better, Faster, Lighter Java  (Bruse Tate 著，bitter java/ejb 作者) 很小本，純講理念的居多.... 我承認我看不去...&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;一 個技術到底受不受歡迎，看廣告、看補習班、或是看 forum (TheServerSide...) 講的天花亂墜都不準。看市面上書的數量最準。用的人多了才會有人肯寫書，Struts 雖然不是什麼 jxxx standard，大伙抱怨也不少，可是書多到嚇死人。現在 Spring 相關書藉一直增加，再過不久就要變 de facto standard 囉，先學先賺先享受。&lt;br /&gt;&lt;br /&gt;&lt;div style="text-align: center;"&gt;oxoxoxoxoxoxoxo Spring IoC 分隔線 xoxoxoxoxoxoxoxoxo&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;回 到 Spring 的正題，IoC。導入 IoC container 到系統... 目前我的感覺是有好有壞，好是好在一旦使用 IoC 的方式開發，程式會漸漸 decouple 並且 high coherence (這個詞真難翻，意思是模組的凝聚力高，該自己負責的功能就自己負責，不會跟其他模組混在一起。跟decouple的意思差不多) 壞是壞在很容易掉入 &lt;a href="http://www.martinfowler.com/bliki/AnemicDomainModel.html"&gt;AnemicDomainModel&lt;/a&gt; 的陷阱。用了 Spring，物件該有的功能/行為，一不小心就會寫到最外面那一層的Service (facade)去了，原因是：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt; service 那一層通常是 injection 的起點，這一層在整個 Application 中是一定要在 Spring 內設定的 (因為通常也是 Transaction 的 boundary )&lt;/li&gt;   &lt;li&gt;再 加上 entity (通常是 PO, persistence object) 很難做 IoC ，為了能讓各種功能 "外包" 出去，自然而然的便在 service 層大興土木 (entity 大多是由 "new" 及 hibernate load from db 時產生的，這兩種生成方式不受 Spring 管，而且通常 entity 數量很大，讓所有 entity inject 相同的 bean 通常會有 performance 的考量) 。&lt;/li&gt;   &lt;li&gt;最後由於 Spring 建議 bean 最好是在 container 裡是 singleton (這點沒錯)所以最好 bean 都要 thread safe，而最簡單的 thread safe 的寫法就是將 object 寫成 stateless...&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt; 這樣的結果是？ -- 會變成 Service 外掛很多的 statless util ，然後 entity 本身只剩下 getter/setter。變成只是個好看一點的 &lt;a href="http://martinfowler.com/eaaCatalog/transactionScript.html"&gt;Transaction Scripts&lt;/a&gt; 。domain object (entity) 連個行為都沒了，變的很 "貧血"。&lt;br /&gt;&lt;br /&gt;為此，又回去翻 domain 聖經 "DDD" (Domain Driven Design) ，有了一些新的感想：原來關鍵還是在 OOP 的設計 (value object 和 entity 合作)。這個等到有比較具體的想法和實際做過後再提吧。&lt;br /&gt;　&lt;br /&gt;　&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-110822125880859258?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/110822125880859258/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=110822125880859258' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110822125880859258'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/110822125880859258'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2005/02/spring.html' title='好久沒寫啦，一堆 Spring 雜感'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-109646991423751096</id><published>2004-09-29T22:22:00.000+08:00</published><updated>2004-09-30T10:02:41.546+08:00</updated><title type='text'>Gmail 小技巧</title><content type='html'>今天不談 java, 來談談前幾天拿到的 Gmail invite 吧。呼~~ 總算可以用這個 hacker's mail 了。&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail 目前只能用 UTF-8 寄信和收信&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;&lt;li&gt;優點：mail 中可同時顯示多國語言&lt;/li&gt;   &lt;li&gt;缺點1：收到 big5 的信時，有時候會有掉字的情形，雖然不大嚴重，但還是個問題啊&lt;/li&gt;   &lt;li&gt;缺 點2：部份 mail client 還不能正確顯示 UTF-8 的信件。我自己試 outlook 2003 是可以。其他舊的就不知道了。除此之外 web mail client 像是 hinet web mail 。因為是 browser based 的，所以會受限於 browser 的能力。用 IE 開啟 hinet web mail 時，UTF-8 的信還是被解讀成 big5..... 就變成亂碼了。而 FireFox 便顯示無誤。&lt;/li&gt; &lt;/ul&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail 目前不支援中文字的查詢&lt;br /&gt;&lt;/span&gt; &lt;ul&gt;   &lt;li&gt;任何中文字的查詢會回傳所有有中文字的信件，換句話說 ==&gt; 不能搜尋中文 &lt;/li&gt; &lt;/ul&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail 的 contacts 使用技巧&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;Gmail 現在已經支援用 .csv 檔直接 import contacts，但是還不支援中文的 import... 這一點真的很麻煩。沒有什麼解決技巧，就是 import 進去後，再把那些亂碼手動一一改回中文。&lt;/li&gt;   &lt;li&gt;目 前 Gmail 的 contacts 還不支援 group，不過還是有方法可以做到的：新建一個新的 contacts，假設是 "friend" 好了，然後 email 處輸入 &lt;pre&gt;mail1@abc.com&amp;gt;,&amp;lt;mail2@gmail.com&amp;gt;,&amp;lt;mail3@xyz.com &lt;/pre&gt;這 樣的格式。注意每個 email 之間都用 &amp;gt;,&amp;lt; 隔開，並且頭尾不要加任何符號，這樣一來，每次你在 寄件人裡輸入 friend 時，它會找到這個 group，並且自動展開成 &lt;pre&gt;&amp;lt;mail1@abc.com &amp;gt;,&amp;lt;mail2@gmail.com&amp;gt;,&amp;lt;mail3@xyz.com&amp;gt;&lt;/pre&gt;，也就是說它會幫你加上頭尾的 &amp;lt; 和 &amp;gt; 。這樣一來就可以直接寄整個群組了&lt;/li&gt;   &lt;li&gt;如 果 import 舊 contacts 很麻煩的話。可以試著用你以前的郵件軟體，寄一封信給你所有的連絡人(要包括你的Gmail address)，信件內容就說明你即將更換新的 email 帳號。然後回到 Gmail 收這封信，然後再將這封信回覆給所有的人，這次信件的內容就可以通知大家你的新 Gmail address 了。 經過這個步驟，Gmail 便會自動將你所有回覆的人加到 contacts 裡。&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail 鍵盤快速鍵：&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;要使用快速鍵，先到 setting 將 &lt;b&gt;Keyboard shortcuts on &lt;/b&gt;打開。&lt;/li&gt;   &lt;li&gt;&lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;j&lt;/span&gt; 和 &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;k&lt;/span&gt; 可以移動游標，而 &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;x&lt;/span&gt; 則可以圈選 check box 。 (跟 vi 很像的啦)&lt;/li&gt;   &lt;li&gt;&lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;u&lt;/span&gt; 可以 update 目前的信箱，這個很常用&lt;/li&gt;   &lt;li&gt;&lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;y&lt;/span&gt; 是 archive，這個 archive 在每個信箱的意義都不一樣喔，想知道可以去查 Gmail 的 &lt;a href="http://gmail.google.com/support/bin/answer.py?answer=6594"&gt;help&lt;/a&gt;&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;&lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;!&lt;/span&gt; 則是 report spam ，這個我也很常按。&lt;/li&gt;   &lt;li&gt;&lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;c&lt;/span&gt; 是 compose, &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;r&lt;/span&gt; 是 reply, &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;a&lt;/span&gt; 是 reply all, &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;f&lt;/span&gt; 是 forward，這四個鍵如果加上 &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;shift&lt;/span&gt;，就會開新的視窗編輯&lt;/li&gt;   &lt;li&gt;複合鍵 &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;gi&lt;/span&gt; --&gt; go to inbox,  &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;ga&lt;/span&gt; --&gt; go to All Mail,  &lt;span style="font-weight: bold; color: rgb(255, 0, 0);"&gt;gs&lt;/span&gt; --&gt; go to starred mail&lt;/li&gt; &lt;/ul&gt;        可能有點難想像，不過只要親自按一按就知道有多好用了！&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail filter 技巧：&lt;/span&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;filter 在進行條件篩選時，可以同時篩選多個 email ，文法如下：&lt;/li&gt; &lt;/ul&gt;          &lt;pre&gt;(my@gmail.com) OR ( xyz@cityhunter.com) OR (test@ggg.com)&lt;/pre&gt;每個 email 都用 () 括住，中間用大寫的 OR 隔開 (一定要大寫)。寫好後會變成像這樣：&lt;br /&gt;&lt;pre&gt;From:((my@gmail.com) OR ( xyz@cityhunter.com) OR (test@ggg.com))&lt;/pre&gt;這樣子只要其中一個 mail 符合就算符合了 (可以用 test search 先試試)&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;filter 各項條件間關係都是 "AND"，也就是說當你設成&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;&lt;pre&gt;From:(xyz@cityhunter.com)&lt;br /&gt;to:(my@gmail.com)&lt;/pre&gt;要兩者同時成立的 mail 才會被篩選出來。目前好像還沒辦法將條件間設成 "OR" 的關係 &lt;ul&gt;   &lt;li&gt;雖然網頁上說最多只能 20 個 filter，其實是可以超過的喔！&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail Address Alias (虛擬 Email )&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;假設你自己的 Gmail address 是  johnson@gmail.com，那麼可以用 "+" 加號來增加虛擬的 email，比方說：&lt;pre&gt;&lt;br /&gt;johnson+amazon@gmail.com&lt;br /&gt;johnson+ebay@gmail.com&lt;br /&gt;johnson+friend@gmail.com&lt;br /&gt;johnson+office@gmail.com&lt;/pre&gt;&lt;br /&gt;像 是第一個代號你可以註冊在 amazon 裡，而第二個可以拿來註冊 ebay，第三個則是給你所有要好的朋友。最後的可以用在公事上。用這些虛擬 email 寄的信仍然會寄到 johnson@gmail.com，好處是你可以很輕易的用 filter 來做分類。比方說：&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;設一個 &lt;pre&gt;to:((johnson+amazon@gmail.com) OR (johnson+ebay@gmail.com)) &lt;/pre&gt;這樣的 filter，然後收到信時 apply 一個 "shopping" 的 label ，如此一來所有 amazon 和 ebay 寄給你的信都會自動加上 shopping 的 label 。&lt;br /&gt;&lt;/li&gt;   &lt;li&gt;同理，再加一個 filter ----  to:(johnson+friend@gmail.com) ，然後 apply "good friend" label.....&lt;/li&gt;   &lt;li&gt;依此類推便可以很巧妙的分類各種信件，而且每個虛擬 email 的使用地方都不大一樣，多少能防止垃圾郵件的氾濫喔。&lt;br /&gt;&lt;/li&gt; &lt;/ul&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail notifier &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;在你的工作列上放個小圖示，信件到來時會發個小通知給你。這個是一定要裝的啊！！你可以在 這裡 &lt;a href="http://toolbar.google.com/gmail-helper/"&gt;下載&lt;/a&gt;。&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail Java API&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;Gmail 已經有 java 的 API 可以使用了，是香港人寫的喔： &lt;a href="http://g4j.sourceforge.net/"&gt;http://g4j.sourceforge.net/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style="font-weight: bold;"&gt;Gmail 不錯的相關資源&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;   &lt;li&gt;輸入 email 就可以拿到 invite：&lt;a href="http://isnoop.net/gmailomatic.php"&gt;invite spooler&lt;/a&gt;，不過別忘了如果有多的 invite 也要記得捐贈喔！&lt;/li&gt;   &lt;li&gt;最大的 &lt;a href="http://www.gmailforums.com/"&gt;Gmail Forum&lt;/a&gt; (英文)&lt;/li&gt;&lt;li&gt;30 個以上Gmail 實用技巧：&lt;a href="http://g04.com/html/modules.php?name=News&amp;amp;new_topic=12"&gt;Jim's Tips.com&lt;/a&gt;  (英文)&lt;/li&gt;&lt;li&gt;Blog：&lt;a href="http://gmailgems.blogspot.com/"&gt;Gmail Gem&lt;/a&gt;  (英文)&lt;/li&gt;   &lt;li&gt;Blog：&lt;a href="http://justinblanton.com/archives/2004/06/20/getting_more_out_of_gmail/"&gt;Getting more out of Gmail&lt;/a&gt;  (英文)&lt;/li&gt;   &lt;li&gt;Blog：&lt;a href="http://noellab.net/ernest/tavi/BobChao/NoteBook/NoteAboutGmail"&gt;Gmail 測試筆記&lt;/a&gt;&lt;/li&gt;    &lt;li&gt;Blog：&lt;a href="http://www.wretch.twbbs.org/blog/elementaler"&gt;Yuchen 的 Gmail 網路日誌&lt;br /&gt; &lt;/a&gt;&lt;/li&gt; &lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;就這樣啦~~ 希望大家也用的愉快&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-109646991423751096?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/109646991423751096/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=109646991423751096' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109646991423751096'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109646991423751096'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/09/gmail.html' title='Gmail 小技巧'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-109163381874818130</id><published>2004-08-04T22:25:00.000+08:00</published><updated>2004-08-05T00:50:10.000+08:00</updated><title type='text'>2004 Javatwo @Taiwan</title><content type='html'>今年也參加囉.. 連同去年已經算兩次了。很不幸的是第二天的下午居然頭忽然劇痛起來，後面的兩個場次就沒聽到了 (spring 沒聽到... ) Anyway, 講一下個人的感想吧：&lt;br /&gt;&lt;br /&gt;good:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;Java girl 讓今年的技術大會比較軟性一點，不像去年整個會場都是硬綁綁的感覺。&lt;/li&gt;&lt;br /&gt;&lt;li&gt;侯sir的 generic/reflection 是去年的延伸，還是一樣學術探討氣味濃厚，整理 JDK 的 Source 讓大家學習。原來 generic 已經全面泛濫到 jdk 1.5 所有的 source 了，而且還有一堆很特別的 &amp;lt;? extends T &amp; comparator&gt; 一些複雜的語法，Tiger 真的有簡化 Java 的開發嗎？呼~~ &lt;/li&gt;&lt;br /&gt;&lt;li&gt;葉秉哲的 concurrent 我個人覺得最棒，難度深淺都有，而且用對照的方式，與會的人馬上就能進入狀況，真是收穫良多呀。lock 和 threadpool 真是迷人的功能，以後都要用新的 util 來寫囉！&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Jini兄講的是 security... 啊，感覺上他準備了很多料和笑話，不過好像臨場經驗不足，都沒有發揮出來，頗為可惜。內容並沒探討很深入的東西，算是幫大家走一遍 JAAS + 複習吧？聽完之後真是覺得目前手上的系統 security 很多都沒考慮清楚... 要重新檢視這一塊囉！最精采的部份就是 SQL code injection，看了之後真是怕怕的！居然有這種方式破解，同行的同事都說回去要趕快試試自己的code！&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Bea 的勞虎。去年我也參加了廠商的場次，是 Novell 的 exteNd，講的是企業系統的整合，Novell 提出的做法就是用 xml 的方式和所有的舊式大型主機做溝通，讓所有的子系統串接起來。今年 SOA 興起，Bea 的做法更是漂亮，event + control項連接所有的子系統，(內部的連接應該都是用 soap) 視覺化的編寫 Struts presentation (預設做出來的就好漂亮了) 真的是大幅簡化開發、整合的難度。Bea 也深知，要在 Java 界為自己的framework 推廣，沒有 open source 是不行的，因此也有了 Beehive 這個 project。Bea 的目的也與 Sun 一樣，希望靠簡化的工具將 Java的餅做大，吸引 jsp/php/asp/vb... etc 的使用者。它的東西我雖然一直無緣採用，不過感覺上公司的策略、方向都很正確，而且很有彈性，說 open 就 open，希望 Bea 能真的實現推廣計畫呀。&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;bad:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;專車班次太少，好像只有一班而已。今年參加人數應該比去年還多吧，就算事先不知道，至少第二天的時候也該加派一個班次吧... 不知是沒人反應還是主辨單位反應太慢。希望明年不要再犯這種錯誤了~&lt;/li&gt;&lt;br /&gt;&lt;li&gt;廠商的攤位是幹嘛的？去年的時候我就覺得很奇怪，除了 macromedia 之外，怎麼廠商的攤位都沒什麼料？沒人在介紹產品，也很少看到有人跟廠商在討論。看到一大群人拿著護照去貼貼紙，領領獎品... 然後就沒了~  感覺廠商只是來要個人資料而已 (免費曝光自己的身份證，email... etc 給廠商，大伙還刷的挺很高興的... ) 希望其他廠商能和 macromedia 看齊，多秀一些獨門絕活，這樣才像技術大會呀&lt;/li&gt;&lt;br /&gt;&lt;li&gt;大家都說包包好... 可是我覺得除了 logo 之外，顏色和造型都頗差的... 而且太大了點吧？&lt;/li&gt;&lt;br /&gt;&lt;li&gt;太貴&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-109163381874818130?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/109163381874818130/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=109163381874818130' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109163381874818130'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109163381874818130'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/08/2004-javatwo-taiwan.html' title='2004 Javatwo @Taiwan'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-109050854793076908</id><published>2004-07-22T22:55:00.000+08:00</published><updated>2004-07-30T13:29:12.263+08:00</updated><title type='text'>My favorite usage of Struts ActionForm</title><content type='html'>First ! my English is very bad.... so... you know what I mean !&lt;br /&gt;&lt;br /&gt;It seems like there are many developers favorite dynamic ActionForm, and there are many discussions with it. For myself, I hate that anything can't check at compile time, and increasing size of struts-config.xml is not so funny. I tend to keep struts-config.xml small and reuse ActionForms as many as possible. The ideas borrow from some books (I forgot the source...)&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class EmailForm extends ActionForm {&lt;br /&gt;    private String emailDisplay ;&lt;br /&gt;    public String getEmailDisplay() {...}&lt;br /&gt;    public void setEmailDisplay(String email) {...}&lt;br /&gt;    public Email getEmail() {&lt;br /&gt;        return new Email(getEmailDisplay());&lt;br /&gt;    }&lt;br /&gt;    public void reset(&lt;br /&gt;           ActionMapping mapping, &lt;br /&gt;           HttpServletRequest request) {&lt;br /&gt;	setEmailDisplay(null);&lt;br /&gt;    }&lt;br /&gt;    public ActionErrors validate(&lt;br /&gt;                ActionMapping mapping,&lt;br /&gt;                HttpServletRequest request) {&lt;br /&gt;        //...&lt;br /&gt;        EmailValidate.check(getEmailDisplay());&lt;br /&gt;        //...&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;above EmailForm do some validation, and convert String emailDisplay to business object "Email" via getEmail(). We can directly call getEmail() in Struts Action to prevent annoy conversion codes. Now let's reuse it:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserForm extends ActionForm {&lt;br /&gt;    private EmailForm emailForm = new EmailForm();&lt;br /&gt;    private String userName ;&lt;br /&gt;&lt;br /&gt;    public String getEmailForm() {...}&lt;br /&gt;    public void setEmailForm(EmailForm emailForm) {...}&lt;br /&gt;&lt;br /&gt;    public Email getContactEmail() {&lt;br /&gt;        return getEmailForm().getEmail();&lt;br /&gt;    }&lt;br /&gt;    public void reset(&lt;br /&gt;            ActionMapping mapping, &lt;br /&gt;            HttpServletRequest request) {&lt;br /&gt;	setUserName(null);&lt;br /&gt;        getEmailForm().reset(mapping,request); // reuse reset()&lt;br /&gt;    }&lt;br /&gt;    public ActionErrors validate(&lt;br /&gt;                ActionMapping mapping,&lt;br /&gt;                HttpServletRequest request) {&lt;br /&gt;        //reuse validate()&lt;br /&gt;        ActionErrors errors &lt;br /&gt;             = getEmailForm().validate(mapping, request); &lt;br /&gt;        // do rest of validation.&lt;br /&gt;        UserName.check(getUserName());&lt;br /&gt;    }&lt;br /&gt;    public String getUserName() {...}&lt;br /&gt;    public void setUserName(String userName) {...}&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;In UserForm, we reuse getEmail(), validate(), and reset() methods of EmailForm. Of course, we can apply another email validation/resetting rule if required. In Struts Action, using delegated getContactEmail() makes better sense. in jsp, just write "emailForm.emailDisplay", as below:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;&amp;lt;html:form ... &gt;&lt;br /&gt; User Name: &amp;lt;html:text property="userName" /&gt;       &lt;br /&gt; Contact Email: &amp;lt;html:text property="emailForm.emailDisplay" /&gt;&lt;br /&gt;&amp;lt;/html:form&gt;&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Quit simple ! As application grow, we may extract and refactor more elemantary ActionForms like EmailForm to reduce code duplication. Delegating ActionForm not only use for fields, but also behaviors (button), for example:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class CRUDForm extends ActionForm {&lt;br /&gt;    private String createButton ;&lt;br /&gt;    private String readButton ;&lt;br /&gt;    private String updateButton ;&lt;br /&gt;    private String deleteButton ;&lt;br /&gt;    public boolean isCreate() {&lt;br /&gt;         return isButtonPressed(getCreateButton());&lt;br /&gt;    }&lt;br /&gt;    public boolean isRead() {&lt;br /&gt;         return isButtonPressed(getReadButton());&lt;br /&gt;    }&lt;br /&gt;    public boolean isUpdate() {&lt;br /&gt;         return isButtonPressed(getUpdateButton());&lt;br /&gt;    }&lt;br /&gt;    public boolean isDelete() {&lt;br /&gt;         return isButtonPressed(getDeleteButton());&lt;br /&gt;    }&lt;br /&gt;    private boolean isButtonPressed(String button) {&lt;br /&gt;       return button != null &lt;br /&gt;&lt;br /&gt;                  || button.trim.length() &gt; 0 &lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    // getter/setter of createButton, readButton,....etc.&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;Above create an elementory CRUD (create/read/update/delete) ActionForm, we can apply it to our UserForm too.&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserForm extends ActionForm {&lt;br /&gt;    private CRUDForm operation = new CRUDForm();&lt;br /&gt;    public void setOperation(CRUDForm operation) {...}&lt;br /&gt;    public CRUDForm getOperation() {...}&lt;br /&gt;    // rest of fields like userName, email... etc&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;while in Action, write some codes like below:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public ActionForward execute(...) {&lt;br /&gt;    UserForm userForm = (UserForm) form;&lt;br /&gt;    if(userForm.getOperation().isCreate()) {&lt;br /&gt;        doCreateUser();&lt;br /&gt;    } else if (userForm.getOperation().isRead() {&lt;br /&gt;        doQueryUser();&lt;br /&gt;    } else ....&lt;br /&gt;    // rest of doXXXUser()....&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;A little messy codes.... But at least we don't need to "extends" DispatchAction or LookupDispatchAction to limit our Struts' Action inheritance (yes, usually we have a generic BaseBusinessAction across all of our Struts Action hierarchy). Addtionally, this avoid hard code string in struts-config.xml or methods such as getKeyMethodMap(...) from LookupDispatchAction. Less hard code string lead less run time error and we can do happy refactoring without worry. As application grow, we may have a big ActionForm called OperationForm which gathers lots of operations like isSubmit(), isCancel(), isCreate(), isWithDraw() ,and isPublish().... etc. to gain maximum of reuse.&lt;br /&gt;&lt;br /&gt;Personally, I prefer to spend more time on building more compact and reusable ActionForms rather than fighting hard code string stuff such as dynamic ActionForm or LookupDispatchAction.&lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-109050854793076908?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/109050854793076908/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=109050854793076908' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109050854793076908'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109050854793076908'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/my-favorite-usage-of-struts-actionform.html' title='My favorite usage of Struts ActionForm'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-109014124916139121</id><published>2004-07-18T16:30:00.000+08:00</published><updated>2004-07-18T17:00:49.163+08:00</updated><title type='text'>好文分享：獨孤木專欄 - Process</title><content type='html'>看看連結吧：&lt;br /&gt;&lt;a href="http://220.130.0.97/javaweek/javaweekly_old.php?ygdd=19#2"&gt;爪哇教室＞ 獨孤木專欄 - Process《上》&lt;/a&gt;&lt;br /&gt;&lt;a href="http://220.130.0.97/javaweek/javaweekly.php?ep_acc=#4"&gt;爪哇教室＞ 獨孤木專欄 - Process《下》&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;寫的真是好啊，舉的例子都是一針見血，都是在我們週遭發生的呀(非資訊業也是如此)。我個人對 process 沒什麼經驗，但是卻很痛恨文件。所以一直很想在 team 裡面推 XP，最少這是文件最少的 process 了。我們自個的 team 雖小，十個人吧，大概不會有大企業的那種阻力，但是其中也是有人獨尊 UML，希望多畫一點... 哈，看來衝突在所難免！&lt;br /&gt;&lt;br /&gt;不過看了 singlelog 的文章後，真的要三思後行啊... 也許真的沒有那種 super process 可以套用所有的專案，目前比較可行的是 "量身訂作" 吧？&lt;br /&gt;&lt;br /&gt;嗯，再想想，再多討論一點吧 &lt;br /&gt;反正我不是做決策的人 ! 輪不到我做決定啊~~~~~ &lt;br /&gt;&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-109014124916139121?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/109014124916139121/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=109014124916139121' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109014124916139121'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109014124916139121'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/process.html' title='好文分享：獨孤木專欄 - Process'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-109006576791013064</id><published>2004-07-18T13:06:00.000+08:00</published><updated>2004-07-18T02:24:53.633+08:00</updated><title type='text'>Object Mother: Automatic register Hibernate persistent object for tear down</title><content type='html'>寫過 Unit Test 的開發人員都曾經為了建置 fixture 而傷透腦筋吧？，尤其是準備資料庫的資料來滿足各式各樣的測試條件時更是頭痛。Object Mother Pattern 就是專為建立複雜的fixture而誕生的，先來看看一個簡單的 Object Mother 吧:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserObjectMother {&lt;br /&gt;    public static User createNonActiveUser() throws Exception {&lt;br /&gt;        return new User("anonymous"); // default non-active&lt;br /&gt;    }&lt;br /&gt;    public static User createActiveUser() throws Exception {&lt;br /&gt;        User anonymous = createNonActiveUser();&lt;br /&gt;        anonymous.setActivate(true);&lt;br /&gt;        return anonymous ;&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;首先，由 createNonActiveUser() 這個 method 可以建立一個未啟用的使用者，接下來的 createActiveUser() 則是先呼叫 createNonAciveUser()，重新設定為啟用的使用者後傳出去。因此，我們可以用UserObjectMother 建立不同條件的使用者以供測試使用。而且我們可以發現 createActiveUser() 重用了 createNonActiveUser() 這個 method。 依這樣的設計延伸，我們可以再寫個 ProejctObjectMother:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class ProjectObjectMother {&lt;br /&gt;    public static Project createProject(String projectName) &lt;br /&gt;       throws Exception {&lt;br /&gt;         Project prj = new Project(projectName);&lt;br /&gt;         prj.setOwner(UserObjectMother.createActiveUser()) ;&lt;br /&gt;         return prj ;&lt;br /&gt;    }&lt;br /&gt;    public static Project createProjectWithMembers() &lt;br /&gt;       throws Exception {&lt;br /&gt;         Project prj = createProject("anonymous prj");&lt;br /&gt;         User[] members = new User[] {&lt;br /&gt;              UserObjectMother.createActiveUser(),&lt;br /&gt;              UserObjectMother.createActiveUser(),&lt;br /&gt;              UserObjectMother.createActiveUser()         &lt;br /&gt;         };&lt;br /&gt;         prj.addAllMembers(members);&lt;br /&gt;         return prj;&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;ProjectObjectMother (以下開始簡稱 ProjectOM, UserOM.. etc) 除了自己產生project物件外，也使用了 UserOM 來幫他建立 owner 和 members。眼尖的讀者應該看出這裡的所有 method 都是 static 的。這是 ObjectMother 的特性使然，我們需要可以隨時隨地的呼叫這些 Mother，並且任意的產生物件。想想看，當我們的 ProjectOM 呼叫 UserOM, 而 OrderOM 又呼叫 UserOM 和 ProjectOM，VendorOM 又要呼叫 OrderOM 和 UserOM... etc ，管理這麼多 OM 的 instance 也是個麻煩事啊！用 static 便能輕易的解決( 這是向 paper 裡學來的喔 )。第二個要注意的是，這些 ObjectMother 的 method 都是 throws Exception，這樣的話才不會因為 business exception 改了之後要修改這些 method，這個和 JUnit 的 TestCase 的 setup() method 一樣，它也是 throw 最大的 Exception (ObjectMother 最常在 setup() 裡被呼叫)。&lt;br /&gt;&lt;br /&gt;OK, 接下來就是重頭戲了：tear down。為了保持 test isolation，我們必須 run 完一個 test case 就 清掉所有產生的物件。從上面的例子看來，我們不需要做任何的清理動作。程式結束後自然會被 garbage collect 掉。比較麻煩的是資料庫裡的持久化資料，因為不做清理的話，會一直影響其他的測試。很多做法是建立一個 insert 的 SQL script，配另一個 delete/drop 的 SQL script。這裡我們採用 Hibernate 的方式來產生資料庫的物件，一方面可重用原來的 createXxx 的 method，另一方面 refactoring 時才不用維護兩套系統。&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserObjectMother {&lt;br /&gt;    &lt;br /&gt;    // other method like createNonActiveUser() &lt;br /&gt;    // and createActiveUser() ....&lt;br /&gt;    &lt;br /&gt;    public static User savedActiveUser() throws Exception {&lt;br /&gt;        User activedUser = createActiveUser();&lt;br /&gt;        Session session = HibernateUtil.currentSession();&lt;br /&gt;        Transaction tx = session.beginTransaction;&lt;br /&gt;        session.save(activedUser);&lt;br /&gt;        tx.commit();&lt;br /&gt;        return activedUser;&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;這裡的 HibernateUtil 是 Thread local pattern 的產物，Hibernate 的網頁有介紹怎麼使用。如果你對這種寫法不熟，請先停止閱讀，先到 Hibernate 網頁一探究竟 (很值得學的) 。Anyway,  savedActiveUser() 這個 method 重用了 createActiveUser()，並且開啟 transaction 將資料寫入資料庫裡，然後回傳一個 persistent object(PO)出去。(注意，本文中的 User, Project... 等等 都是 Hibernate 的 PO) 如此，test case 便能測試和資料庫相關的功能。好了，接下來我們要清理掉這個存過的物件。最簡單的做法就是自己寫：&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserTest extends TestCase {&lt;br /&gt;    User anonymous ;&lt;br /&gt;    public void setup() throws Exception {&lt;br /&gt;        anonymous = UserObjectMother.savedActiveUser();&lt;br /&gt;    }&lt;br /&gt;    public void tearDown() {&lt;br /&gt;        // begin transaction code.&lt;br /&gt;        session.delete(anonymous);&lt;br /&gt;        // commit transaction code.&lt;br /&gt;    }&lt;br /&gt;    public void testActiveUserChangeProject() {&lt;br /&gt;       // do some business test...&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;在tearDown()裡，我們用 delete() 來清理PO。如果這麼簡單，就不用 pattern 來解決囉。當 UserOM, ProjectOM, OrderOM 不斷產生新的 PO 之後，tear down 將複雜到完全無法撰寫。這時我們要進入 Object Mother pattern 的核心 -- 註冊那些新儲存的 PO，然後到後面再一口氣清掉。我們先來看看在概念上是怎麼用的吧：&lt;br /&gt;&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserTest extends TestCase {&lt;br /&gt;    User anonymous ;&lt;br /&gt;    Project newProject ;&lt;br /&gt;    public void setup() throws Exception {&lt;br /&gt;        anonymous = UserObjectMother.savedActiveUser();&lt;br /&gt;&lt;br /&gt;        // savedProject() call createProject &lt;br /&gt;        // then save into database.&lt;br /&gt;        newProject &lt;br /&gt;             = ProjectObjectMother.savedProject("new proejct");&lt;br /&gt;    }&lt;br /&gt;    public void tearDown() {&lt;br /&gt;        // purge all saved PO, including "anonymous" &lt;br /&gt;        // and "newProject"&lt;br /&gt;        RegisteredObject.getInstance().purge();&lt;br /&gt;    }&lt;br /&gt;    public void testActiveUserChangeProject() {&lt;br /&gt;       anonymous.changeProejct(newProject);&lt;br /&gt;       // do some business test...&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;tearDown() 只變一行了，而且不論我們用多少種 UserOM, ProjectOM... ，都只要call purge() 一次就可以清掉。我們來看看怎麼實做吧，首先，要達到 call 一次 purge() 就將所有的 PO 都清除掉，我們需要一個 singleton:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class RegisteredObject {&lt;br /&gt;&lt;br /&gt;    private static final RegisteredObject instance &lt;br /&gt;                                   = new RegisteredObject();&lt;br /&gt;&lt;br /&gt;    private RegisteredObject() {}&lt;br /&gt;    private static RegisteredObject getInstance() {&lt;br /&gt;        return instance;&lt;br /&gt;    }&lt;br /&gt;    private List allRegisteredPOs = new ArrayList();&lt;br /&gt;    public void add(Object entity) {&lt;br /&gt;        allRegisteredPOs.add(entity);&lt;br /&gt;    }&lt;br /&gt;    public void purge() throws Exception {&lt;br /&gt;        HibernateUtil.closeSession();  // this step is important.&lt;br /&gt;        if(allRegisteredPOs.isEmpty()) {&lt;br /&gt;             return ;&lt;br /&gt;        }&lt;br /&gt;        Transaction tx = null;        &lt;br /&gt;        try {&lt;br /&gt;            Session session = HibernateUtil.currentSession();&lt;br /&gt;            tx = session.beginTransaction;&lt;br /&gt;&lt;br /&gt;            // FILO, first in last out to delete PO&lt;br /&gt;            for(int i = allRegisteredPOs.size()-1;i &gt;= 0;i--) {&lt;br /&gt;                 session.delete(allRegisteredPOs.get(i));&lt;br /&gt;            }&lt;br /&gt;&lt;br /&gt;            tx.commit();&lt;br /&gt;        } catch (Exception e) {&lt;br /&gt;            tx.rollback();&lt;br /&gt;        } finally {&lt;br /&gt;            allRegisteredPOs.clear();&lt;br /&gt;            HibernateUtil.closeSession();&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;我們可以看到 RegisteredObject 是個 singleton 而且裡面有個 List allRegisteredPOs 來儲存所有註冊的 PO。然後在 purge() 裡面將所有註冊的 PO 全部清掉。purge 很長，我們等一會再看，我們先看看在 Hibernate 中要如何註冊新儲存過的 PO:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class RegisterInterceptor implements Interceptor {&lt;br /&gt;    private RegisteredObject registeredObject;&lt;br /&gt;&lt;br /&gt;    public RegisterInterceptor(RegisteredObject reg) {&lt;br /&gt;        this.registeredObject = reg ;&lt;br /&gt;    }&lt;br /&gt;    public boolean onSave(Object entity, &lt;br /&gt;                          Serializable id, &lt;br /&gt;                          Object[] state,&lt;br /&gt;                          String[] propertyNames, &lt;br /&gt;                          Type[] types) &lt;br /&gt;                          throws CallbackException {&lt;br /&gt;&lt;br /&gt;        registeredObject.add(entity);&lt;br /&gt;        return false;&lt;br /&gt;    }&lt;br /&gt;    // rest of methods: like onLoad(), onDelete().... etc.&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;RegisterInterceptor，一個 Hibernate 的 Interceptor(攔截器)。Hibernate 的攔截器 可以對 persistent 的生命週期做一些特殊的處理，功能有點像是 database 的 trigger。以這裡的 RegisterInterceptor來說，在 PO save 前的一刻，它會呼叫 onSave()這個 method。我們利用這點將該 entity 註冊到 registeredObject 裡，如此一來就可以獲得所有新儲存的 PO 了。有了 Interceptor，下一步就是安裝在 Hibernate 的 session 裡，這樣子只要是透過 session 儲存的 PO 都會被我們攔截下來並註冊。&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class HibernateUtil {&lt;br /&gt;&lt;br /&gt;    private static Interceptor interceptor ;&lt;br /&gt;    public static setInterceptor(Interceptor i) {&lt;br /&gt;         interceptor = i;&lt;br /&gt;    }&lt;br /&gt;&lt;br /&gt;    private static final ThreadLocal threadLocalSession &lt;br /&gt;                                          = new ThreadLocal();&lt;br /&gt;    &lt;br /&gt;    public static Session currentSession() &lt;br /&gt;                          throws HibernateException {&lt;br /&gt;        try {&lt;br /&gt;            Session s = (Session) threadLocalSession.get();&lt;br /&gt;            // Open a new Session, if this Thread has none yet&lt;br /&gt;            if (s == null) {&lt;br /&gt;                // add interceptor if it has one.&lt;br /&gt;                if (interceptor == null) {&lt;br /&gt;                    s = sessionFactory.openSession();&lt;br /&gt;                } else {                      &lt;br /&gt;                    s = sessionFactory.openSession(interceptor);&lt;br /&gt;                }&lt;br /&gt;                threadLocalSession.set(s);&lt;br /&gt;            }&lt;br /&gt;            return s;&lt;br /&gt;        } catch (HibernateException e) {&lt;br /&gt;            logger.fatal("error in getting a session", e);&lt;br /&gt;            throw e;&lt;br /&gt;        }&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;上面是 HibernateUtil 的一小部份。我們加了 setInterceptor() 的 method，讓我們從外部設定 interceptor。而 currentSession() 裡，我們多了sessionFactory.openSession(interceptor); 這一行。所以只要我們設定 interceptor 之後，HibernateUitl.currentSession() 所回傳的 Hibernate session 都會加裝攔截器，在這裡我們當然會設定為 RegisterInterceptor 囉！我們在看看改寫過的 UserTest:&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class UserTest extends TestCase {&lt;br /&gt;    User anonymous ;&lt;br /&gt;    Project newProject ;&lt;br /&gt;    public void setup() throws Exception {&lt;br /&gt;        // create interceptor&lt;br /&gt;        RegisiterInterceptor interceptor &lt;br /&gt;             = new RegisiterInterceptor(&lt;br /&gt;                  RegisteredObject.getInstance());&lt;br /&gt;&lt;br /&gt;        // setup interceptor for all created session&lt;br /&gt;        HibernateUtil.setInterceptor(interceptor);&lt;br /&gt;&lt;br /&gt;        anonymous = UserObjectMother.savedActiveUser();&lt;br /&gt;        newProject = &lt;br /&gt;               ProjectObjectMother.savedProject("new proejct");&lt;br /&gt;    }&lt;br /&gt;    public void tearDown() {&lt;br /&gt;        RegisteredObject.getInstance().purge();&lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;setup 裡多了兩行，一行是 new 一個 interceptor (用 singleton RegisteredObject 當容器)，第二行是將interceptor設在 HibernateUtil 裡。如此一來，所有經由 HibernateUtil 拿到的 session 都會對儲存的物件註冊；而到了 tearDown()，再執行 RegisteredObject 的 purge() 就可以全部清掉了。好，現在回過頭來看 purge() 是怎麼寫的。&lt;br /&gt;&lt;br /&gt;首先 purge() 的 第一行就是 closeSession() ，這一行是必要的，因為在測試的 method 中，有可能會開第二個 session 去 load 第一個 session 所產生的 PO 。依我們之前的設計，第一個 session 所產生的 PO (session1PO) 通常都是由 ObjectMother save 的，同時也被攔截下來放到 RegisteredObject 裡。這時，如果第二個 session 如果又去 load 同一個 PO (session2PO)，這樣執行 session.delete(session1PO) 時，Hibernate 會 throw exception 說不能刪除尚有 session 連結的 PO (session2PO 還在跟 session2 連著)，因此我們要先將 session2PO "離線"，將session close，使它也變成 detached 的物件。這樣我們才能將 session1PO (這個也是 detached) 順利的 delete() 掉。&lt;br /&gt;&lt;br /&gt;第二個就是 delete 的順序。在這裡我們採用 FILO 先進後出的順序來 delete() PO。這個是與 Database 的 foriegn key (FK) 有關。當 insert 資料到 database 時，一定是depend 最多 FK 的物件先 insert ，才會輪到 FK 物件 insert，依此類推，最後 insert 的物件是完全不會有被 FK 指向到的，因此這樣的物件是可以先 delete 掉，而不會有 FK 衝突。因此我們清除的順序是先將後到的 PO delete，再漸漸的往前推，直到最早 save 的那一個物件為止。&lt;br /&gt;&lt;br /&gt;第三個是 finally 裡面的 allRegisteredPOs.clear(); 因為 RegisteredObject 是 singleton ，我們每次執行 purge 之後就要將註冊的內容清除，不然一個個 test case 執行下去會讓 allRegisteredPOs 越來越大，也不合理。&lt;br /&gt;&lt;br /&gt;最後，這裡還有一個隱含的功能：按 FILO 的順序是沒辦法正常清除掉可以 null 的 FK 關係的。因為他們兩者都可以先獨立儲存 (不分順序) ，然後再連成 FK 的關係，這樣一來按 FILO 刪除就沒意義了，因為可能要先移除的反而先儲存，順序就錯了。好在 Hibernate 的 "Transactional write behind" 可以輕鬆解決這個問題。只要在同一 transaction 裡，Hibernate 會知道誰要先清掉，然後重新安排 delete 的順序。也許你會說那不就不用 FILO 了嗎？ FIFO 就可以了啊.... etc。這裡我只能說，經實驗證明，是不可行的。原因是什麼我也不清楚。也許是 Hibernate 內部的排序上有極限，又有可能是即使 PO 間有互相 FK 的資訊，理論上也是不可能完全到推出當初儲存的順序，後者比較有可能吧.... 我想。&lt;br /&gt;&lt;br /&gt;Summary:&lt;br /&gt;這篇我們實作了二個簡單的 ObjectMother: UserOM 和 ProjectOM，而在 UserTest 裡我們使用它們在 setup() 和 tearDown() 裡。以 static method 的方式來實作 ObjectMother 可以讓我們在 setup() 或者是其他建立 fixture 的地方任意產生物件。對於資料庫相關的物件我們使用 Hibernate PO 的方式來建立。為確保所有 ObjectMother 所產生的 PO 都能正確、完全的清理，我們建立了 RegisteredObject 這個 singleton 來存放所有欲清理的 PO。而RegisterInterceptor 這個攔截器則攔截所有要儲存的 PO，註冊至 RegisteredObject 裡。最後經由 FILO 及 transactional write behind 的方式在 tearDown 處 purge 掉所有新儲存的 PO。藉由 Hibernate，讓 database 資料的 setup tearDown 更為簡單，又完成了一項不可能的任務！Again, Hibernate Rocks !&lt;br /&gt;&lt;br /&gt;note:&lt;br /&gt;RegisteredObject, RegisterInterceptor 兩者是 ObjectMother 該有的功能，全部整合在一個 BaseObjectMother 裡，然後讓 UserObjectMother, ProjectObjectMother... etc 去繼承它，這樣才算是完成了 Object Mother Pattern，這個留給讀者回去自己實作吧。&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-109006576791013064?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/109006576791013064/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=109006576791013064' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109006576791013064'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/109006576791013064'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/object-mother-automatic-register.html' title='Object Mother: Automatic register Hibernate persistent object for tear down'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108983024890725139</id><published>2004-07-15T02:21:00.000+08:00</published><updated>2004-07-15T02:44:01.846+08:00</updated><title type='text'>Object Mother Pattern 優缺點</title><content type='html'>最近有人提到 Object Mother ，嗯，寫一下近來的發現吧&lt;br /&gt;&lt;br /&gt;先講講缺點：&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;Object Mother (OM) 的理念是可以註冊要清理掉的物件，以便在 tear down 的時候可以全部 purge。其實這個理念並不好 implement，目前也沒看過有什麼 framework 可以幫的上忙的。我自己的話多是用在 database相關的物件生成，一方面是 DB 的資料準備很複雜，另一方面是 Hibernate 的 session.delete(object) 在 purge 的時候幫了大忙。即使如此，註冊 object 還是要手動進行。手續很繁瑣...&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;OM 和 test code 本身 "距離太遠" ，造成 test code 閱讀困難。距離就是 test code 和 OM 之間相隔的 method和class 數。當 developer 看到 test code 時，他還要額外的去翻那個 OM 出來看，才知道到底產生的物件長什麼樣子，横跨兩至三個class，太遠了。如果 OM 又呼叫其他的 OM，那距離又更遠了，可讀性變更差...&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;距離遠帶來的不僅是程式易讀性的問題，還會造成 test 過慢。我曾經實做過 Fixture 最大有 call 到三層的 OM，而且每一層都會 hit database，喔~~ my God ! 才十個 test case 總共要 run 30 秒以上。&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;上面提到像是連call好幾層的 OM，其實已經超過 unit test 的範圍，等於是在做 integration test了。而且有個嚴重的問題，就是當 OM 層層呼叫時，有時最前面的錯誤會一直累積到最後面還不會發現... 因為後面的 test case 都是一直根據前面的建立的 "範圍" 下產生的。例如最前面的資料沒有cover負值的範圍，後面的 test case 如果一直按這個 OM 產生的資料寫測試。久而久之就會出現後面的 class 沒測到負值的範圍(或者是 side effect)。這個實務上我碰過一次，當時真的是嚇呆了。因為沒想到會在最前面藏著這麼一個 bug.&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;br /&gt;嘿，換來講講優點吧：&lt;br /&gt;&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;寫了這麼多 OM 發現到，其實清理物件也是 test 的項目之一啊！常常會發現如果無法正常的 tear down ，那大概是程式有哪裡忘了關 resource (例如 IO 忘了 close, 或兩個 object 同時 reference 到同一個 hibernate PO) 所以花時間寫 OM 的註冊和 purge 其實是值得的，因為 release resource 也是程式中重要的一環啊。&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;OM 在 team 裡面很好用的，只要有一個人寫出來，大家都可以用，不用每個人都需要知道怎麼正確產生 object 的各種 state。&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;上面說到了，OM 就是迷你的 integration test，相信這付出的額外代價會有回報的。&lt;/li&gt;&lt;br /&gt;&lt;br /&gt;&lt;li&gt;極困難測試的變成可以測！測試寫的再慢再難懂也比沒有測試強上一大截，這是最重要的啊~ :-)&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;有機會再寫一些 Object Mother + Hibernate 的做法吧 (其實很單純.... 沒什麼了不起....)&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108983024890725139?l=xexex.blogspot.com'/&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108983024890725139/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='https://www.blogger.com/comment.g?blogID=6833744&amp;postID=108983024890725139' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108983024890725139'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108983024890725139'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/object-mother-pattern.html' title='Object Mother Pattern 優缺點'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='17692352117638435950'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry></feed>