<?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' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-6833744</id><updated>2011-04-22T02:11:41.319+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?max-results=100'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>42</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>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' alt='' /&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='http://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:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108938211154550305</id><published>2004-07-09T21:22:00.000+08:00</published><updated>2004-07-10T10:10:48.240+08:00</updated><title type='text'>Architech and Domain</title><content type='html'>這個標題取的很大，其實不是要講這麼厲害的事。從轉行到資訊業已經過了四個月了，對我來說這是第一個接觸到開發計劃 (Project) 的進行，而且內容是偏向服務性質的。這個算是資訊業的 "本行" 吧，以前雖然寫過工程應用方面的程式，不過都是兼職的性質在寫，而且是一個人做好玩的。哪有什麼時程壓力？需求變更？現在手上這個計劃目前為止大約完成一半吧... 雖然還沒有跑完整個開發流程，現在已經有很多感想。&lt;br /&gt;&lt;br /&gt;Java 語言的技術，不敢說自己很好，但是對絕大多數的問題大多可以解決。再往上一層，OO 觀念和design pattern ，這個還不夠好，不過自己覺得也有一點鄒型了，只差更多的經驗累積。這四個月來讓我思考最多的便是 architech 和 domain -- 打造一個 Agile 的 system。以前買的書都是 thinking in java, Design pattern Java workbook, jsp/servlet... etc. 現在開始買了一些 Domain Driven Design, The Design of Site, Agile Database Technique, Unit Test in Java.... 等等屬於比較 "上一層思考" 的書。其中 Hibernate in Action 和 Domain Driven Design 給我很大的影響。&lt;br /&gt;&lt;br /&gt;我自己覺得真的很幸運，我們的單位選用 Hibernate，而且正當緊鑼密鼓開發的時候，就出了 Hiberante In Action 這本書。對一個 project 開發的新新手來說等於是有一個名師在指點你最好的 practice。第一次學就用最好的，而不是走偏走遠了，繞了一大圈才恍然大悟。&lt;br /&gt;&lt;br /&gt;anyway.... 我們單位目前沒 SA / SD， programmer 要自己搞定。其實還好要自己搞，不然我一定整天跟 SA/SD吵架，而且也沒有學習的機會了，況且在時程的壓力並沒有很大的情況下，還有 "驗證" 設計問題的空間咧。話說回來，有時候像是書上還沒看到的部份 (Hibernate in Action 還沒出完, Domain Driven Design 則太厚了..) 自己會先想一些方法去實作。後來在書上看到了一樣的 implemenation 時，心裡真是爽啊！會推導出與書上一樣的結論，表示現在的自己思考回路是往正確的方向在走啊！&lt;br /&gt;&lt;br /&gt;之前我曾經做過有關 Hiberante architech 的一些小結論。現在再整理一些東西吧：&lt;br /&gt;&lt;br /&gt;&lt;ul&gt; &lt;br /&gt;&lt;li&gt;&lt;b&gt;Entity&lt;/b&gt; Entity 就是帶有 id 的 class ，有 id 表示在 business 的意義上，你會希望能夠辨別它與別人不同，而且能夠獨立的存在，不依附其他 class，像是 user, project, order... 之類的。Entity 通常都需要儲存 (persistent) 它的 state，稍後在重新 load 出來繼續工作，在 Hibernate 裡就是 PO 了，每個PO 都會有自己的 id。而且身為 Domain的 Entity，PO 理所當然要有business 的行為囉，像是 project.assignJob() 之類的method都會在 PO 出現，不要只把 PO 當做 DTO 啊 (DTO, data transfer object, 只有 getter/setter，純用來傳遞的物件) &lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;b&gt;Value Object (VO) &lt;/b&gt; VO 跟 Entity 是相對應的，它沒有 id，多與 Entity, VO 協同工作，而完成任務後，就被 garbage collect 掉了。像這樣的 class 因為物件不具識辨性，所以最好是設計成 immutable 且 side effect free (因為生成多少個都不影響功能，可以看我之前寫的 &lt;a href="http://xexex.blogspot.com/2004/06/immutable.html"&gt;Immutable 症候群&lt;/a&gt;)。當它依附在 Entity 之下時，像是 user 下的 address，它也會跟著 persistent，這時在 Hibernate 裡就是用 component的方式實做。&lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;b&gt;Aggregate&lt;/b&gt; Aggregate 就是 domain 裡面相關的東西結合起來的一個class族群，例如車子是由外殼、輪胎、引掣.... 等 class 組成。裡面這些 class 可能有 Entity(車子) 也有 VO(輪胎)。而重點是 Root of Aggregate (在這裡就是車子)的觀念，這個 Root 是這個 Aggregate 的對外唯一窗口，要存取車子的第三個輪胎，一定得通過 "車子" 這個物件來使用才行。唯有這樣，車子外部的使用者才不會接觸到車子底層的實作，未來在變更時才有彈性。這也表示在設計一個 Aggregate 時，Aggregate 一定要在 domain 上有意義才行 (未來的變更一定是依照 domain 的特性去變的，跟採用的IT技術無關)。而 Aggregate 在 Hibernate 裡就是用一個 Entity (PO) 當做 root，然後對其他的子 class 做 foreign key 連結(many-to-one, one-to-many... etc) 形成一個以 root 為中心的 Object graph. 比方說以 project 當做 root，而要存取 project 所有成員的地址，就要先取得 project 之後，再利用project.getAllMemberAddresses() 的方式來讀取。而不是另外找了一堆 user object，湊一湊 user.getAddress() 了事 &lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;b&gt;Repository&lt;/b&gt; repository 就是類似 DAO 一樣的元件，方便做 CRUD 和 findByXxx()的動作。但是和傳統DAO做法不同的是，並不是一個 table 一個 DAO class，而是一個 Aggregate 一個 Repository，cover 的範圍以 domain 為單位。像是車子內的這幾個class 不會有自己的 repository ，只有 root 車子才會有 CarRepository，這個與上面的 Aggregate 相互應：root class 是對外的唯一窗口。在 domain 裡，我們不可能不找車子，而直接找第三個輪胎。如果胡亂寫個 WheelRepository 的話，不通過車子任意讓人讀取，這會造成架構的混亂，未來車子要變更設計時... 頭就很大了！anyway, 在 Hibernate 裡實作 repository 非常容易，像是儲存車子資料好了：&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class CarRepository {&lt;br /&gt;    public add(Car car) {&lt;br /&gt;        session.save(car); &lt;br /&gt;    }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;其他的子 class 的 persistence code 都不用寫了，直接寫在 *.hbm.xml 裡： 將每個 Aggregate 的連結 (one-to-many, many-to-one...) 設好 cascade="all" 或是 "save-update"... 等。 這樣就 ok 了，輕鬆愉快，不用再寫 session.save(wheel), save(engine)...etc 這類瑣碎的code 了。像這樣子 coarse 的設計，也有助於 test；只要寫個 MockCarRepository，這樣外部的 code 就不用 hit database 也能做車子資料相關的測試，也不用再 mock 一堆 MockWheelRepository, MockEngineRepository... 了，這只會增加 test 難度而已&lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;b&gt;Service with Domain IOC&lt;/b&gt; Service是統合所有流程的地方，裡面會有數個 class 協同運作，這時我們可以利用 IOC (Inverse of Control, 又稱 dependency injection) 將 CarService 設計為 ：&lt;br /&gt;&lt;pre&gt;&lt;br /&gt;public class CarService {&lt;br /&gt;     public CarService(CarCleanUtil cleanUtil, &lt;br /&gt;                       CarRepository repository, &lt;br /&gt;                       ChargeManager manager) {&lt;br /&gt;         // some construction...&lt;br /&gt;     }&lt;br /&gt;     public void washCar(Long carId) {&lt;br /&gt;          Car car = repository.findById(carId);&lt;br /&gt;          int cost = car.washBy(cleanUtil);&lt;br /&gt;          manager.charge(car.getOwner(), cost);&lt;br /&gt;          repository.updateCarRecord(car);&lt;br /&gt;          //.... etc&lt;br /&gt;     }&lt;br /&gt;}&lt;br /&gt;&lt;/pre&gt;&lt;br /&gt;一些 CarService 會使用到的相關工具，資料，或服務，全部從 constructor 丟進來，(這種做法稱constructor injection) 這樣一來你可以輕鬆切換各種 implementation，像是可以隨時變換另一種清潔工具 (CarCleanUtil)，也可以更改付費方式 (ChargeManager)，再者可以換掉車子資料的來源 (CarRepository)。這樣的做法在測試時更具優勢，我們可以做個 MockCarReposotory 這樣就可以任意做出各種車子的資料而不用 hit database。也可以做個 FailChargeManger 專門用來測試是錢不夠的狀況... etc.  啊？不知道什麼東西要丟到 constructor 外面嗎？試試看 test-first 吧，很快就會發現的。不過要小心，往外丟的 class 最好是在 domain上具有意義。有個簡單的方法可以檢查：替這個 class 取個 domain 裡用的名詞，如果無法用 domain裡的名詞命名，那特意分離出去就沒意義啦！因為未來會改變 implementation 一定是來自 domain 的需求。舉個例，假設目前 CarService 裡面需要寄 email 給客戶，我們把mail相關的部份拿出去變成 &lt;pre&gt;    public CarService(EmailUtil emailUtil) &lt;/pre&gt;，這樣是不錯，但是 EmailUtil 太過於 technical 了，跟 domain 比較無關。在這裡 domain的目的不外乎是為了傳達訊息給客戶，我們可以改成客戶的連絡 &lt;pre&gt;    public CarService(CustomerContact contact) &lt;/pre&gt;然後實作一個 EmailContact 給目前的需求使用。這樣一來以後要改用手機簡訊連絡也沒問題！&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;還有一個漏掉的是 Factory，到目前為止我用的還不是很多(大多是用 constructor 就搞定了)所以沒什麼心得。上述這些觀念多半是從 Domain-Driven Design 這本書學來的，經過實際與 Hibernate 並用後的一些心得。Domain-Driven Design 提到的觀念 和 Hibernate 的設計理念有很多雷同之處，因此 Hibernate 可以很簡單的 fit 這樣的架構。或者我們可以這樣說：Hibernate 幫助我們完成這樣的架構。&lt;br /&gt;&lt;br /&gt;Again,  Hibernate Rocks !&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108938211154550305?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108938211154550305/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108938211154550305' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108938211154550305'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108938211154550305'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/architech-and-domain.html' title='Architech and Domain'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108878983490306041</id><published>2004-07-03T01:21:00.000+08:00</published><updated>2004-07-03T01:37:14.903+08:00</updated><title type='text'>Dark comic</title><content type='html'>現在流行的漫畫.....  好像都那麼點黑暗。像是 ハガレン (鋼之鍊金術士) 就是講 等價交換 與 人體鍊成這種黑暗體裁。故事中不乏合成 人 + 狗 變成怪獸，或者是合成人變成異形死屍... 太黑暗了! 還有另外一部 death note (死亡筆記本) 則是死神讓人可以任意殺人...  故事雖然鬥智精彩，而且目前好像蠻賣座的... 但是.... 殺人耶！ 不論故事要表達什麼，玩弄人命是要付出代價的，漫畫都畫的輕描淡寫。 &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-108878983490306041?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108878983490306041/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108878983490306041' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108878983490306041'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108878983490306041'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/07/dark-comic.html' title='Dark comic'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108773082739053255</id><published>2004-06-20T19:17:00.000+08:00</published><updated>2004-06-20T19:33:58.230+08:00</updated><title type='text'>Hibernate Rocks</title><content type='html'>Hibernate in action 第五章出了，講的是 cache, concurrency 和 transaction，拜讀之後真是對 Hibernate 五體投地，什麼事都幫你設想好，只要在 hbm 設一設就搞定了。真的是 transparent persistent 無所不在。Hibernate in action 也提供了一些 concurrency guideline，不然那個什麼 read uncommited / read commited / serializable.... etc 真的很繁鎖，而且又難。這對我們這種半調子 programmer 真的算的上是天上掉下來的禮物。用 Hibernate:&lt;br /&gt;&lt;br /&gt;  (1) making difficult work extremely easy, making impossible task possible.&lt;br /&gt;  (2) 用慣了 Hibernate 會把你變成 persistent 白吃 :-P&lt;br /&gt;  (3) A true vendor lock-in ! 因為無法想像沒有 Hibernate 要怎麼開發~~ 呼呼&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108773082739053255?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108773082739053255/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108773082739053255' title='3 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108773082739053255'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108773082739053255'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/06/hibernate-rocks.html' title='Hibernate Rocks'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108748332863823525</id><published>2004-06-17T21:57:00.000+08:00</published><updated>2004-06-18T00:39:39.393+08:00</updated><title type='text'>Test infected</title><content type='html'>Test infected, 這個是 Test 大師 Kent Beck 形容 "非寫 test，不然就沒辦法 coding" 的一種習慣。這個字該怎麼翻好呢？  "被測試感染"、"感染到測試"、"後天性測試症候群"、"急性測試炎".... -_-;;  好像太離譜了~~ 還是等待大師來翻譯正名吧.&lt;br /&gt;&lt;br /&gt;Anyway, 這個禮拜又玩了一下 test-first 理論。這次從兩個方向著手，第一個是 service oriented 的程式。service 嘛，因為只知道它要做什麼，怎麼個做法還不知道，所以自然是從上到下 (top-down) 的方式來開發，先從最外面的輪廓著手：&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip#1]&lt;br /&gt;public class DataService {&lt;br /&gt;　　 public void sendData(String data) {;}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;(note: test 比這個早寫完)&lt;br /&gt;接下來開始工作讓這個 service 工作，這個 service 會送資料到某個檔案，所以就變成這樣：&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip#2  心中閃過的念頭，假的]&lt;br /&gt;public class FileDataService {&lt;br /&gt;　　public FileDataService(String fileName) {&lt;br /&gt;　　　　this.fileName = fileName ;&lt;br /&gt;　　　　//etc....&lt;br /&gt;　　}&lt;br /&gt;　　public void sendData(String data) {&lt;br /&gt;　　　　String newFormat = formatConversion(data);&lt;br /&gt;　　　　new FileWriter(fileName).write(.....)&lt;br /&gt;　　　　//etc&lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;其實這個 FileDataService 並沒有真的寫出來，因為先寫 test 就知道 ==&gt; 有夠難測！為了要 assert 這個程式的結果，我還要去找個真的檔案來寫，寫完後再來個 FileReader，讀完了之後，再把這個檔案給 purge 掉，暈倒... 為了讓 test 好寫，就將 fileName 拿掉，改用 OutputStream 當成介面：&lt;br /&gt;&lt;br /&gt;//[code snip#3  test寫完後的第二個版本]&lt;br /&gt;public class DataService {&lt;br /&gt;　　public DataService(OutputStream out) {&lt;br /&gt;　　　　this.out = out ;&lt;br /&gt;　　　　//etc....&lt;br /&gt;　　}&lt;br /&gt;　　public void sendData(String data) {&lt;br /&gt;　　　　String newFormat = formatConversion(data);&lt;br /&gt;　　　　out.write(...);&lt;br /&gt;　　　　//etc&lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;呼呼，這下 output 的來源被丟出去了，改成從 constructor 進來，這就叫 constructor injection 吧？測試這個程式就簡單多啦，我可以 new ByteArrayOutputStream() 給它，然後就.... 嘿，這個留給看倌自己測試啦，這個 class 很好用地~~ (hint: toByteArray() )。&lt;br /&gt;&lt;br /&gt;後來這個程式演變成：&lt;br /&gt;//[code snip#4  第三個版本]&lt;br /&gt;public class DataService {&lt;br /&gt;　　public DataService(OutputFactory factory) {&lt;br /&gt;　　　　this.factory = factory ;&lt;br /&gt;　　　　//etc....&lt;br /&gt;　　}&lt;br /&gt;　　public void sendData(String data) {&lt;br /&gt;　　　　String newFormat = formatConversion(data);&lt;br /&gt;　　　　OutputStream out = factory.createOutputStream();&lt;br /&gt;　　　　out.write(...);&lt;br /&gt;　　　　//etc&lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;用一個factory來建立新的 OutputStream，因為這樣這個 service 才能送資料到不同地方，而這個 DataService 在進行測試時，其實 OutputFactory 還沒寫好，只是使用一個 dummy 的 MockOutputFactory 來輔助測試，DataService 才不管要寫到哪裡去咧，這是 OutputFactory 的事。它專心做它該做的事 (data conversion, write data... etc) 。由上面這個例子可知 test-first 帶來了： &lt;strong&gt;逼你去想好測試的方法，把不相干的東西盡量丟到外部去&lt;/strong&gt;，唯有這樣 Object Isolation 才會好。&lt;br /&gt;&lt;br /&gt;Ok, 現在進入第二個例子：bottom-up，就是由小到大，從細部做起：我想設計一個 TableModel，有 cell, 也有 row，然後可以定義 cell 的 format，而且還有一個特殊功能，可以合併 table。由小到大，先寫 cell 、再寫 row、最後寫 Table&lt;br /&gt;&lt;br /&gt;//[code snip#5  由小到大]&lt;br /&gt;public class DataCell {&lt;br /&gt;　　public DataCell(Double data, DecimalFormat format) {;}&lt;br /&gt;}&lt;br /&gt;public class DataRow {&lt;br /&gt;　　public DataRow(int initialSize) {;}&lt;br /&gt;　　public void addCell(DataCell cell);&lt;br /&gt;　　public void appendRow(DataRow rowToCombine);&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;上面是寫完的結果，步驟是這樣地：首先先寫 DataCell 的 test，再完成 DataCell 本身 (DataCell設計成 immutable) 。有了 DataCell 之後，就開始寫 DataRow 的 test，這個 test 就好寫了，因為已經有"測試完整"的 DataCell 可用，不用費心去做 Mock。而那個特別的 appendRow() method，只做了個大概，這是想到等一下要用來合併 Table的 "原料"，也許用不著也說不定。有了 DataRow 就可以拿來寫 DataTable 了：&lt;br /&gt;&lt;br /&gt;//[code snip#6  完成 DataTable]&lt;br /&gt;public class DataTable {&lt;br /&gt;　　public DataTable(int initialRow, int initialColumn) {;}&lt;br /&gt;　　public void addRow(DataRow row);&lt;br /&gt;　　public void appendTable(DataTable tableToCombine);&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;同樣，DataTable 因為有 DataRow 可用，所以寫來很快。而那個 appendTable 在測試的時候，不斷的去 refactor DataRowTest 和 DataRow 的 appendRow() method，以符合 DataTable 的需求 (比如說同樣的 column 數才能合併之類的條件) &lt;br /&gt;&lt;br /&gt;從小到大的撰寫，正常來講除非設計的很完善，知道每個小元件該做什麼，才比較好實施，不然常常寫到大的部份才要重翻會瘋掉... 通常 "常識"之類的設計需求，例如本例中的 Table，大多數的狀況 Table 都是由 row 和 cell 組成的；又或者是車子，大部份都有 engine、輪胎等 parts ... 像這樣子的比較適合吧，因為這種架構都很固定了。&lt;br /&gt;&lt;br /&gt;回到正題.... test-first 在這裡也發揮了功能: 我寫完這 DataCell, DataRow, DataTable 時，&lt;strong&gt;完全沒有做任何的 debug !&lt;/strong&gt; 每一個 class 都是建立在完整測試好的子元件上。當 DataRow 的 appendRow() 不夠 Table的 appendTable 用時，先增加/修改 DataRowTest 的 assertion，然後再修改 DataRow 直到綠燈亮為止。這樣確保 appendTable 去call appendRow時一定不會出錯，也就是撰寫 test 永遠比 refactoring 先做。我雖然不知道如果沒寫 test 的話會發生什麼奇怪的臭蟲，不過這種不用 debug 就寫完程式，這可是寫程式以來還沒遇過的事啊~~~~ 而且完成度還很高 (有一堆 test 掛保證 :-) &lt;br /&gt;&lt;br /&gt;不論是 bottom-up, top-down， test first 都有地方可以發揮功能。top-down 需要用點小技巧來做 isolation，通常寫完之後會留下一堆 mock 或 dummy class ，而 bottom-up 比較不會，但是需要事先良好的設計。而且等到子元件越疊越大時，還是得做 mock，不然建立 fixture 的時候會吐血 (也可以用 Object Mother pattern 來解決啦)&lt;br /&gt;&lt;br /&gt;Conclusion.... &lt;strong&gt;I have been test infected !&lt;/strong&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108748332863823525?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108748332863823525/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108748332863823525' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108748332863823525'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108748332863823525'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/06/test-infected.html' title='Test infected'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108661774745359844</id><published>2004-06-07T21:11:00.000+08:00</published><updated>2004-06-08T00:11:50.693+08:00</updated><title type='text'>強迫性 Immutable 症候群</title><content type='html'>自從看了 Domain-Driven Design 這本書之後，唉~ 不知該說好還是壞。就說裡面提到的 Side-effect free function 吧。side-effect free指的是 object 是 immutable 的，做了改變不會影響到本身也不會氾濫到其他的 object，而 function 則是指這個 method 可以執行多次而結果都一樣 (數學的 function 都是這樣) &lt;br /&gt;直覺式的錢包計算：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip #1]&lt;br /&gt;public class MoneyBag {&lt;br /&gt;　　private int asset ;&lt;br /&gt;　　public void spend(int expenditure) {&lt;br /&gt;　　　　asset -= expenditure; &lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;這樣夠嗎？當然不夠，還要加 precodition 和 post-condition:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip #2]&lt;br /&gt;public class MoneyBag {&lt;br /&gt;　　private int asset ;&lt;br /&gt;　　public void spend(int expenditure) {&lt;br /&gt;　　　　if(expenditure &lt;= 0) &lt;br /&gt;　　　　　　throw new IllegalArgumentException();&lt;br /&gt;　　　　final int diff = asset - expenditure; &lt;br /&gt;　　　　if(diff &lt; 0){&lt;br /&gt;　　　　　　throw new InsuffienctMoneyException();&lt;br /&gt;　　　　}&lt;br /&gt;　　　　asset = diff;&lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;夠醜吧，真正的 logic 只有一行，卻弄成啊哩啊扎的&lt;br /&gt;如果另外寫個 Immutable Value Object "Money":&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip #3]&lt;br /&gt;public class Money {&lt;br /&gt;　　private int dollar ;&lt;br /&gt;　　public Money(int dollar) {&lt;br /&gt;　　　　if(dollar &lt;= 0) &lt;br /&gt;　　　　　throw new IllegalArgumentException();&lt;br /&gt;　　　　this.dollar = dollar ;&lt;br /&gt;　　}&lt;br /&gt;　　public Money minus(Money money) {&lt;br /&gt;　　　　int diff = this.dollar - money.dollar ;&lt;br /&gt;　　　　if(diff &lt; 0){&lt;br /&gt;　　　　　throw new InsuffienctMoneyException();&lt;br /&gt;　　　　}&lt;br /&gt;　　　　return new Money(diff) ;&lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;MoneyBag 就不用再煩惱 "錢" 的問題了，變得很簡捷：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip #4]&lt;br /&gt;public class MoneyBag {&lt;br /&gt;　　private Money asset;&lt;br /&gt;　　public void spend(Money expenditure) {&lt;br /&gt;　　　　asset = asset.minus(expenditure); &lt;br /&gt;　　}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;spend() 變得超直覺的 ==&gt; "剩下的錢等於現在的錢 minus 消費額" &lt;br /&gt;再看另一個例子：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//[code snip #5]&lt;br /&gt;public Money calculateDiscount(Money expenditure) {&lt;br /&gt;　　Money allowance = expenditure.multply(0.9) ;&lt;br /&gt;　　return expenditure.minus(allowance);&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;不會有人看不懂這個method在做什麼吧.... 已經非常非常白話了。&lt;br /&gt;&lt;ul&gt;&lt;li&gt;minus() 這個 method 重用了&lt;br /&gt;　&lt;li&gt;隨便你怎麼 call、怎麼組合、call多少次都不會錯&lt;br /&gt;　&lt;li&gt;核心邏輯可讀性很好，接手維護的人會感動到哭&lt;/ul&gt;&lt;br /&gt;這些優點都是歸功於 Side-effect free function 的設計理念。真是棒呆了......　呃... 事情若這麼順利就好了。自從看完這個章節後.... 我的眼中只容的下 Immutable Object ! 哇咧~~ 一看到有什麼 method 不是 immutable 的，就想做 refactoring。再加上功力又不太夠，沒辦法很正確的判斷，搞得 原來的 class 一團亂 -_-;;。而且，本來嘛，那個 MoneyBag 是一個 class 就搞定了，現在要多生一個 Money class 出來，Immutable 好像天生就很容易增加 class 數量的樣子... 這樣到後來也不怎麼好維護吧？！&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-108661774745359844?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108661774745359844/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108661774745359844' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108661774745359844'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108661774745359844'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/06/immutable.html' title='強迫性 Immutable 症候群'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108549635212345433</id><published>2004-05-25T22:31:00.000+08:00</published><updated>2004-05-25T22:45:52.123+08:00</updated><title type='text'>Builder Pattern...</title><content type='html'>From Design Pattern Java workbook, it mentions that a builder can be written as:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;    Builder b = new Builder() ;&lt;br /&gt;    new Parser(b).parse();&lt;br /&gt;    result = b.build();&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;I have spent whole day to design these two classes. It looks like very simple to implement, but test-case is a little bit hard to code. parse() itself is a very coarse-grained method. there are too many assertXxxx() for this method. Another approach is that following JUnit in Action's practice, using package-private instead of full-private for each fine-grained method.   By this, one may test every single step if making test-case place in the same package. &lt;br /&gt;&lt;br /&gt;Which  one is best ?  still no idea.... &lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108549635212345433?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108549635212345433/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108549635212345433' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108549635212345433'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108549635212345433'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/builder-pattern.html' title='Builder Pattern...'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108532366896377229</id><published>2004-05-23T22:36:00.000+08:00</published><updated>2004-05-23T22:55:25.273+08:00</updated><title type='text'>English English</title><content type='html'>Hey, as my eyes and head are still not work properly, this may be a good chancne to enhance my English hearing (is this word right ?)  Today I have bought Ez-talk magazine and it has homeworks for whole month. I hear the homework of June, 1. but.... I can't realize what the mp3s are talking about ! Sigh, how poor I am. &lt;br /&gt;Again, it's time to forbid myself to programming at home. Maybe playing some other interesting (?) stuff like enjoying music or speaking more English every day.... maybe&lt;br /&gt;&lt;br /&gt;I hope I can do it.&lt;br /&gt;&lt;br /&gt;A guy said that he want Ctrl-z, Ctrl-s, Del key in real life...&lt;br /&gt;whoa ! That's my dream, too ! If life has ctrl-z and undo up to 100 level.... Hehe guess how the life can be. &lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108532366896377229?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108532366896377229/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108532366896377229' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108532366896377229'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108532366896377229'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/english-english.html' title='English English'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108506230084402541</id><published>2004-05-20T21:40:00.000+08:00</published><updated>2004-05-20T22:11:40.846+08:00</updated><title type='text'>cliffs of Dover</title><content type='html'>cliffs of Dover - 多佛的峭壁&lt;br /&gt;雖然搞不清楚這個地方有什麼特別的，不過這一首 guitar solo 已經變成我的最愛囉！傳說中在 1986? Eric Johnson 以這一首震攝在場所有聽眾。我不大會描述啦，不過真的真的很好聽~~ 想聽的人... 用 edonkey/emule 吧~&lt;br /&gt;&lt;br /&gt;Hibernate 至今尚在摸索中，到目前為此感覺上自己學的不是很徹底.... 直到讀了第三章，還有兩次的顧問的分享，現在才對 Hibernate 的 Transparent Persistence 有一點 sense 。 這個精神真的是 Hibernate 很重要的一環，follow 這個精神，還可以讓 Domain model 更健全。做夢也沒想到會有這種情形，如果一個 domain model 很難跟 persistence 分開的話，那麼這個 model 本身可能在設計上有所缺陷。前幾遍提到 Hibernate 需要一個 service layer 來輔助 PO as BO 的不足，而這個 service layer 便是要處理所有 session/transaction management 的地方。而 PO 在運作時，則需要在 mapping document 裡設定 persistence 的行為，尤其是 cascade 最為重要。現在比較煩惱的是 service 和那些 access class (find, findByDate... etc) test 很難寫，而且因為涉及 database 所以速度很慢. &lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108506230084402541?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108506230084402541/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108506230084402541' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108506230084402541'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108506230084402541'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/cliffs-of-dover.html' title='cliffs of Dover'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108497695575479706</id><published>2004-05-19T22:09:00.000+08:00</published><updated>2004-05-19T22:31:20.226+08:00</updated><title type='text'>Hibernate in Action Chapter 3</title><content type='html'>Wow !&lt;br /&gt;&lt;br /&gt;  I have read chapter.3 tonight, and it is quite awesone ! I strongly recommend all of Hibernate users to read chapter.3 in depth. &lt;br /&gt;&lt;ul&gt;&lt;li&gt;啊呀！原來 component 是這樣用的！&lt;br /&gt;&lt;li&gt;靠！搞了半天 bidirection 要這樣設定！inverse="true" 讓 Hibernate 知道你的 association 是 bidiretional 的。而且兩邊都設定只會有一次 update，而不雙向設定還不會 update ！&lt;br /&gt;&lt;li&gt;嘿嘿！果然 Table per class hierarchy 才是正解，我果然有先見之明。&lt;br /&gt;&lt;li&gt;啊~~~~ persistence concern 要完全放在 PO 之外！完了，我以前寫的 PO 還會 throw HibernateException 咧... 這下慘了....&lt;br /&gt;&lt;li&gt;啊咧？ accessor 可以動耶，只有 collection 的 accessor 不可以去改。accesor 只會去比 value，所以改了不會亂下 update。不過呢，根據以往經驗，動了 accessor 每次query 的時候好像會多 query 一次。所以還是少用好了.&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-108497695575479706?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108497695575479706/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108497695575479706' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108497695575479706'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108497695575479706'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/hibernate-in-action-chapter-3.html' title='Hibernate in Action Chapter 3'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108463846663373505</id><published>2004-05-16T00:09:00.000+08:00</published><updated>2004-05-16T00:27:46.633+08:00</updated><title type='text'>The design of sites</title><content type='html'>今天去逛天瓏，沒找到那本 Hibernate: The developer's notebook，反而找到一本 Web design 的書藉： &lt;a href="http://www.amazon.com/exec/obidos/tg/detail/-/020172149X/qid=1084636497/sr=8-1/ref=sr_8_xs_ap_i1_xgl14/002-5955354-4208023?v=glance&amp;s=books&amp;n=507846"&gt;The design of sites&lt;/a&gt; 這本書算得上是奇書了，雖然是寫給 web designer 看的，整本書的感覺跟 software 的 design pattern/engineering 沒兩樣。書中建議 designer 使用 iterative 的方式開發，而且整個設計都要注重 customer 的需求/習慣...etc (customer-cetered-design)。這簡直是 XP 了嘛！書的後半部都是 web design pattern ，羅列各種設計的問題所需的解答，現還沒開始看，想必很實用~ &lt;br /&gt;現在翻了兩三章，也吸收到不少的菁華的觀念，像是 low-fidelity prototype、Customers who don't return... etc，真的是買對了！ &lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108463846663373505?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108463846663373505/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108463846663373505' title='0 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108463846663373505'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108463846663373505'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/design-of-sites.html' title='The design of sites'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108454807795618262</id><published>2004-05-14T22:42:00.000+08:00</published><updated>2004-05-14T23:27:30.256+08:00</updated><title type='text'>Simple Business Layer archetect with Hibernate</title><content type='html'>Blogger 換新裝了，新UI好看許多。可惜的是文章區太窄了，這樣寫 CODE 不方便呀。&lt;br /&gt;&lt;br /&gt;Anyway... 不知是染上了什麼病症，已經一個禮拜都處於頭暈的狀態。只能用 50% 的CPU usage 寫程式。哇咧，寫到後來都不知自己在寫什麼~~~ &lt;br /&gt;&lt;br /&gt;Anyway (again...) 本週排了一次 code review，consultant 提了一些建議讓我為之一亮。前幾個禮拜研究 Hiberante PO as BO 時略有心得。不過漸漸的發現 PO 是能夠做到 BO 的功能，但是卻沒有辦法進一步 coporate 其他的 BO。也就是說 BO 和 BO 間需要由一個 service facade 做為橋樑。而這個 facade 便是 presentation 端該看到的。所以我想簡單的 business archetect 可分為三層：&lt;br /&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Service Layer&lt;/strong&gt; -- 這個是最上層，只接受 user 的 input，回傳則是 PO 的object graph。內部負責溝通各個 BO/utility/或甚至是 Service 本身，而 security 由這裡控管。&lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;strong&gt;Access Layer&lt;/strong&gt; -- 這個負責 PO 的 loading，也就是從 database 裡還原持久的物件，典型的 method 像是 find(id), findAll, findByDate.... etc. 都是屬於此類。&lt;/li&gt;&lt;br /&gt;&lt;li&gt;&lt;strong&gt;Domain Layer&lt;/strong&gt; -- 這裡就是最底層了，大多數是 PO，跟 database 的 schema 綁在一起。PO 的 setter 應該設計成最少是 default，最好都是 private權限。要修改這些 PO 得通過另外撰寫的 business method處理。當然這些 business interface 只處理該 PO 本身。&lt;/li&gt;&lt;/ul&gt;&lt;br /&gt;Service Layer 回傳給 presentation 就不轉成 bean 了，直接丟 PO 的 object graph 出去，在 JSP 端直接 navigation (例如: &lt;code&gt; &amp;lt;bean:write name="user" property="parent.address.street"/&amp;gt; &lt;/code&gt; ) 不用再寫一堆 &lt;b&gt;只用一次&lt;/b&gt;的 java bean 了。&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108454807795618262?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://xexex.blogspot.com/feeds/108454807795618262/comments/default' title='張貼意見'/><link rel='replies' type='text/html' href='http://www.blogger.com/comment.g?blogID=6833744&amp;postID=108454807795618262' title='2 個意見'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108454807795618262'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108454807795618262'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/simple-business-layer-archetect-with.html' title='Simple Business Layer archetect with Hibernate'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108339589336910215</id><published>2004-05-01T14:53:00.000+08:00</published><updated>2004-05-01T16:04:16.623+08:00</updated><title type='text'>周りがどんなに騒いでいても、彼が声を出すと皆が静まりかえる</title><content type='html'>Pageable query 真是寫的太痛苦了，一旦對 hibernate 的 query 動手腳後，就變得很難延伸.... 同一種 query ，有時要回傳 full list，有時則是回傳 pageable list... 情況有點類似需要動用到 Bridge pattern。有夠難寫....... 先放棄，等有空再回來想。來談談今天的正題：&lt;br /&gt;&lt;br /&gt;今天又跑去看  &lt;strong&gt;&lt;a href="http://picnic.to/~ohp/review/2000/beck.htm"&gt;BECK&lt;/a&gt;&lt;/strong&gt; 一次了。真是奇怪，這個漫畫非常吸引我，看了一次又一次。內容是講一個中學生的 搖滾樂團 的成長故事，這可不是一般灑狗血的熱血漫畫。反而是反諷日本主流音樂(流行音樂)的糜糜之音，而真正的好音樂是不需要大量的行銷包裝的，漫畫中最棒的場景就是主角 "小雄" 唱歌！只要主角一出聲，整個場景全部凍結，所有人都像被電到一樣震憾！不管週圍有多吵、別人對主角有什麼偏見，全部都被動人的歌聲所震攝。漫畫是沒有聲音的，作者卻有辦法將主角的聲音 "畫" 了出來，另一方面也表達真正的好音樂可以昇華人心。也許過沒幾天又會跑去看一次也說不定~~ 哈！&lt;br /&gt;&lt;br /&gt;至於書名 BECK，其實在漫畫中是&lt;a href="http://store.yahoo.co.jp/robochris/30014.html"&gt;這一隻狗&lt;/a&gt;的名字&lt;br /&gt;&lt;br /&gt;Anyway... 搖滾樂我一點都不喜歡.... 我不喜歡聽用吼的方式來唱歌... heavy metal 也太吵了，吵得令人受不了..... 話說回來，我最喜歡的樂器卻是電吉他，比起歌聲，純電吉他的聲音更能讓我感動！真想買一把回來玩~~&lt;br /&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108339589336910215?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108339589336910215'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108339589336910215'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/05/blog-post.html' title='周りがどんなに騒いでいても、彼が声を出すと皆が静まりかえる'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108324300655834289</id><published>2004-04-29T20:12:00.000+08:00</published><updated>2004-04-29T20:54:23.826+08:00</updated><title type='text'>Pageable DisplayTag with Hibernate</title><content type='html'>啊~~~~~~ 昨、今兩天都跟 DisplayTag 大戰。搞了好久總算搞定了。DisplayTag 這個 taglib 寫的真很不錯，介面很好看，而且 pageable, sort, group 使用上都很簡單。最後還有無限變化的 decorator，設計的人真的非常有經驗！然而它還是有一些地方還不夠完美。使用它的 page 功能，比方說120筆分12頁，每頁10筆好了，它沒有機制可以讓你一次只讀10筆。一定要一次讀完 120 筆才行。這樣分頁的用意就少了一大半了！這兩天就都花在這玩意上，浪費了好多時間。最後的答案還算差強人意。&lt;br /&gt;&lt;code&gt;&lt;br /&gt;HibernatePageList page = new HibernatePageList(pageSize, "from Foo f where f.id &gt;20" ) ;&lt;br /&gt;page.loadPage(2) ;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;HibernatePageList 是一個帶有 iterator() method 的 list (不全是，沒有 implement List) 。這個物件裡面會有空的 list，裡面全部裝滿 null，整個 list 的大小就是 上面 "from... &gt;20" query 出來的筆數。當執行 loadPage(2) 時，內部會去抽第二頁所需的資料，然後 HibernatePageList這個物件的內部的List 的第二頁會被填滿(其餘保持 null) 。如此當使用者點到分頁的第二頁時，在 servlet/action 裡執行 loadPage(2)，就可以讀取需要顯示的值。&lt;br /&gt;&lt;br /&gt;displayTag 本身並沒有提供讀取目前是第幾個分頁的功能。我們只好從它的 request parameter 下手；當 displayTag 分頁時，比如說第三頁，它會在 url 後面接上 "&amp;d-12345-p=3" 這樣的 parameter 。12345 是它產生的隨機碼，有時是四個數字。抓取這個數字可用 regular expression :   params.match("d-[0-9]+-p") 這個 pattern 來找，找到後便可傳給 loadPage. 所以最後可簡化為：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;page.loadPage(request) ; // get current pageNo from request&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;完整的程式碼如下：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;public class HibernatePageList {&lt;br /&gt;&lt;br /&gt;	private List cachedList;&lt;br /&gt;	private int pageNo;&lt;br /&gt;	private int pageSize;&lt;br /&gt;	private int totalSize;&lt;br /&gt;	private String queryString;&lt;br /&gt;&lt;br /&gt;	public HibernatePageList(int pageSize, int totalSize, String queryString) {&lt;br /&gt;&lt;br /&gt;		this.queryString = queryString;&lt;br /&gt;		this.pageSize = pageSize;&lt;br /&gt;		this.totalSize = totalSize;&lt;br /&gt;&lt;br /&gt;		// initialize full list with null;&lt;br /&gt;		cachedList = new ArrayList(totalSize);&lt;br /&gt;		for (int i = 0; i &lt; totalSize; i++) {&lt;br /&gt;			cachedList.add(null);&lt;br /&gt;		}&lt;br /&gt;	}&lt;br /&gt;	public HibernatePageList(int pageSize, String fromBasedQueryString)&lt;br /&gt;		throws HibernateException {&lt;br /&gt;		this(&lt;br /&gt;			pageSize,&lt;br /&gt;			getTotalSize(fromBasedQueryString),&lt;br /&gt;			fromBasedQueryString);&lt;br /&gt;	}&lt;br /&gt;	private static int getTotalSize(String fromBasedQueryString)&lt;br /&gt;		throws HibernateException {&lt;br /&gt;&lt;br /&gt;		return (&lt;br /&gt;			(Integer) HibernateUtil&lt;br /&gt;				.currentSession()&lt;br /&gt;				.iterate("select count(*) " + fromBasedQueryString)&lt;br /&gt;				.next())&lt;br /&gt;			.intValue();&lt;br /&gt;	}&lt;br /&gt;	public void loadPage(final int pageNo) throws HibernateException {&lt;br /&gt;		//check cache has data:&lt;br /&gt;		if (pageIsCached(pageNo)) {&lt;br /&gt;			return;&lt;br /&gt;		}&lt;br /&gt;		Query query = HibernateUtil.currentSession().createQuery(queryString);&lt;br /&gt;		query.setMaxResults(pageSize);&lt;br /&gt;		query.setFirstResult(getPageStartRow(pageNo));&lt;br /&gt;&lt;br /&gt;		final Iterator iter = query.iterate();&lt;br /&gt;		for (int i = getPageStartRow(pageNo); iter.hasNext(); i++) {&lt;br /&gt;			cachedList.set(i, iter.next());&lt;br /&gt;		}&lt;br /&gt;	}&lt;br /&gt;&lt;br /&gt;	public void loadPage(HttpServletRequest request)&lt;br /&gt;		throws HibernateException {&lt;br /&gt;		loadPage(getDisplayTagPageNo(request));&lt;br /&gt;	}&lt;br /&gt;&lt;br /&gt;	/**&lt;br /&gt;	 * extract page no from request of display tag&lt;br /&gt;	 * &lt;br /&gt;	 * @param request&lt;br /&gt;	 * @return&lt;br /&gt;	 * @throws NumberFormatException&lt;br /&gt;	 */&lt;br /&gt;	public int getDisplayTagPageNo(HttpServletRequest request)&lt;br /&gt;		throws NumberFormatException {&lt;br /&gt;		Enumeration names = request.getParameterNames();&lt;br /&gt;		int pageNo = 1;&lt;br /&gt;		while (names.hasMoreElements()) {&lt;br /&gt;			String parameter = (String) names.nextElement();&lt;br /&gt;			if (parameter.matches("d-[0-9]+-p")) {&lt;br /&gt;				pageNo = Integer.parseInt(request.getParameter(parameter));&lt;br /&gt;				break;&lt;br /&gt;			}&lt;br /&gt;		}&lt;br /&gt;		return pageNo;&lt;br /&gt;	}&lt;br /&gt;	private int getPageEndRow(int pageNo) {&lt;br /&gt;		return Math.min(pageNo * pageSize, totalSize);&lt;br /&gt;	}&lt;br /&gt;	private int getPageStartRow(int pageNo) {&lt;br /&gt;		return (pageNo - 1) * pageSize;&lt;br /&gt;	}&lt;br /&gt;&lt;br /&gt;	private boolean pageIsCached(final int pageNo) {&lt;br /&gt;		boolean pageIsCached = true;&lt;br /&gt;		for (int i = getPageStartRow(pageNo);&lt;br /&gt;			i &lt; getPageEndRow(pageNo) &amp;&amp; pageIsCached;&lt;br /&gt;			i++) {&lt;br /&gt;			if (cachedList.get(i) == null) {&lt;br /&gt;				pageIsCached = false;&lt;br /&gt;			}&lt;br /&gt;		}&lt;br /&gt;		return pageIsCached;&lt;br /&gt;	}&lt;br /&gt;	public Object get(int index) {&lt;br /&gt;		return cachedList.get(index);&lt;br /&gt;	}&lt;br /&gt;	public boolean isEmpty() {&lt;br /&gt;		return cachedList.isEmpty();&lt;br /&gt;	}&lt;br /&gt;	public Iterator iterator() {&lt;br /&gt;		return cachedList.iterator();&lt;br /&gt;	}&lt;br /&gt;	public int size() {&lt;br /&gt;		return cachedList.size();&lt;br /&gt;	}&lt;br /&gt;	public int getPageSize() {&lt;br /&gt;		return pageSize;&lt;br /&gt;	}&lt;br /&gt;	public String getQueryString() {&lt;br /&gt;		return queryString;&lt;br /&gt;	}&lt;br /&gt;}&lt;br /&gt;&lt;/code&gt;&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108324300655834289?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108324300655834289'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108324300655834289'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/04/pageable-displaytag-with-hibernate.html' title='Pageable DisplayTag with Hibernate'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108306604223652921</id><published>2004-04-27T19:24:00.000+08:00</published><updated>2004-04-27T21:43:05.763+08:00</updated><title type='text'>How to apply test-first metholody for unknown ?</title><content type='html'>這句話今天一直在腦海中響著。早上一開始就跟 servlet 的檔案下載奮鬥，這玩意寫了好幾遍了。最後一次寫是用 servlet 產生 JFreeChart 的圖。不過每次都忘得一乾二淨，尤其這次的下載，檔名也是 requirement 之一。最後東翻西翻想到的作法是：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;//Foo is a utility to convert request parameter to object.&lt;br /&gt;ServletDownloadable download = Foo.build(request); &lt;br /&gt;response.setHeader("Content-Disposition",&lt;br /&gt;　　　　　　"attachment; filename=" + download.getFileName());&lt;br /&gt;response.setContentType(download.getContentType());&lt;br /&gt;download.write(response.getOutputStream());&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;interface ServletDownloadable 是今天奮戰的結晶，有了這個之後，Business Component 只要 implement 這個 interface，就能重覆使用這個 servlet，整個就只要四行程式碼就夠了。這算是 跛腳 的 IOC 吧？畢竟 implement ServletDownloadable 時還是要寫一些 filename, contentType等 servlet 資源的東東，不過最少已經可以不用寫很難的 servlet test (in-container)。問題來囉！在還不知道確切的寫法前，根本想不出要怎麼先寫 test ，想 test 也沒個底。我心中有個聲音這樣說 --- "YES，我的程度還不夠，對API 還不熟，所以沒辦法先寫 test，先把基礎功打好再說。"  可是總覺得不是滋味。寫程式可以說每天都在試新的東西耶，怎麼可能對每個 API 都熟了後才開始寫。所以大半的情況下，都會查書，抄範例，寫個小小的程式來實做。等到學會這個 API 怎麼用時，自己的程式也大概有一點雛型了。這時當然是將這個雛型寫成完成品啦。這中間的流程，test-first 好像很難找到插入點 (程式都完成一半了，還能叫 test-first 嗎?)  &lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108306604223652921?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108306604223652921'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108306604223652921'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/04/how-to-apply-test-first-metholody-for.html' title='How to apply test-first metholody for unknown ?'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108298158710211959</id><published>2004-04-26T19:57:00.000+08:00</published><updated>2004-04-26T23:57:33.826+08:00</updated><title type='text'>Immutable Object and Test-Driven Development</title><content type='html'>今天又有新發現！原本是想寫一個 number range 的 parser, 例如 使用者輸入 "128~130,140" 時，parser 會回傳 128,129,130,140 四個數字。直覺的寫法就是寫個 parser 的工具：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;        NumberRangeParser parser = new NumberRangeParser() ;&lt;br /&gt;        List results = parser.parse("128~130,140"); &lt;br /&gt;&lt;/code&gt;&lt;br /&gt;後來想說直接將 "128~130,140" 直接轉成 SQL: &lt;code&gt;(foo between 128 and 130) or ( foo = 140) &lt;/code&gt;&lt;br /&gt;啊咧？怎麼好像又要多做一個工，而且還要多寫一個 converter。那API 不就變的很醜，而且很囉嗦.....  不過似乎沒什麼選擇，還是寫吧。嘿！來試試 Test-Driven Development，先寫個 test 再來寫 code。&lt;br /&gt;&lt;br /&gt; ......怪怪，這樣不好，嗯.... 這樣改比較好....  喔，這邊可以寫成這樣....&lt;br /&gt;&lt;br /&gt;經過一連串的 design refactoring ( code 還沒寫 ) ，最後 API 有了驚人的轉變：&lt;br /&gt;&lt;code&gt;&lt;br /&gt;           NumberRange numberRange = new NumberRange("128~130,140") ;&lt;br /&gt;           List numbers = numberRange.toNumberList();&lt;br /&gt;           String SQL = numberRange.toSQL(String foo);&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Whoa ! &lt;em&gt;&lt;b&gt;A range of numbers become an &lt;a href="http://www.javaworld.com.tw/jute/post/view?bid=32&amp;id=30265&amp;sty=1&amp;tpg=1&amp;age=0"&gt;immutable&lt;/a&gt; Object !&lt;/b&gt;&lt;/em&gt; &lt;br /&gt;&lt;br /&gt;在原本的設計裡 parser 只是一個 tools 而已，這就像是寫 procedure code 的 subroutine 一樣，原本的寫法就算是全寫成 static 照樣可以運作。新的寫法則完全是物件的思維，那一串的 number range 整個變成物件，然後可以轉換成各種格式。而且以後在每個 class 之間使用時，都是直接傳 NumberRange ，而不是用 number 的 List 來傳遞，不僅僅防呆，而且界面的彈性更好！最後這個 class 是設計成 immutable 的，換句話說，這個 class test 完後，就永遠不會再錯啦！WOW！而且這個 class 感覺好像 String class 喔，心中突然覺得很爽，自己也能寫出類似大師的程式！&lt;br /&gt;&lt;br /&gt;現在還不清楚這是不是 test-first 帶來的好處。嘿嘿！明天再來試試！&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108298158710211959?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108298158710211959'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108298158710211959'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/04/immutable-object-and-test-driven.html' title='Immutable Object and Test-Driven Development'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108289891722906677</id><published>2004-04-25T21:37:00.000+08:00</published><updated>2004-04-26T23:46:30.700+08:00</updated><title type='text'>Some experiences with unit test...... and Object Mother Pattern</title><content type='html'>之前 &lt;a href="http://www.manning.com/massol"&gt;JUnit in action&lt;/a&gt; 唸了半本，當時想想這樣開發程式真是完美啊！所有的 component 都是用 interface 隔的一乾二淨的。然後每個 component 跟其他的 component 之間 都是用 IOC (&lt;a href="http://www.martinfowler.com/articles/injection.html"&gt;Inversion of Control&lt;/a&gt;) 設定。這樣開發的時候就可以用一堆 &lt;a href="http://www.mockobjects.com/wiki/"&gt;mock&lt;/a&gt;，而不用再準備一大堆的 &lt;a href="http://junit.sourceforge.net/doc/faq/faq.htm#tests_2"&gt;test fixture&lt;/a&gt;。起初唸完還興奮的不得了說.......  不過直到最近..... 開始真正使用 Junit 之後，惡夢才真的開始！&lt;br /&gt;&lt;br /&gt;惡夢中體會到的:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;  &lt;li&gt; &lt;em&gt;&lt;b&gt;Not all components need to apply interface !&lt;/b&gt;&lt;/em&gt; 如果幻想每個 component 都用 interface.... 那就會有管不完的 interfaces ！不暈倒才怪！應該是只有環境 (例如 file system, connection.... etc) 以及各子系統間才需要額外添加 interface.&lt;br /&gt;  &lt;li&gt; &lt;em&gt;&lt;b&gt;For mass test fixture, Use &lt;a href="http://c2.com/cgi/wiki?ObjectMother"&gt;Object Mother Pattern&lt;/a&gt; !&lt;/b&gt;&lt;/em&gt;  這個 pattern 真是棒！不僅可以管理/重用辛辛苦苦建立的 test fixture ，也可以有效的清掉(teardown)建立的 fixture. 而且發現搭配 &lt;a href="http://www.hibernate.org"&gt;Hibernate&lt;/a&gt; 後使用起來更是爽！&lt;br /&gt;  &lt;li&gt; &lt;em&gt;&lt;b&gt;I don't write any code today !! &lt;/b&gt;&lt;/em&gt;終於！我也犯了全天下 unit tester 都會犯的錯。居然一整天都在 write/refactor test case, fixture, 還有那個 Object 媽媽...... 媽媽咪啊！Business code 一行也沒寫到！&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-108289891722906677?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108289891722906677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108289891722906677'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/04/some-experiences-with-unit-test-and.html' title='Some experiences with unit test...... and Object Mother Pattern'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry><entry><id>tag:blogger.com,1999:blog-6833744.post-108289768481247768</id><published>2004-04-25T20:53:00.000+08:00</published><updated>2004-04-26T23:47:31.716+08:00</updated><title type='text'>A Blog Day</title><content type='html'>   測試 blog...... &lt;br /&gt;   從小就沒有寫日記的習慣....... 嘿！看看可以寫幾篇！&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/6833744-108289768481247768?l=xexex.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108289768481247768'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/6833744/posts/default/108289768481247768'/><link rel='alternate' type='text/html' href='http://xexex.blogspot.com/2004/04/blog-day.html' title='A Blog Day'/><author><name>ingramchen</name><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author></entry></feed>
