星期四, 4月 29, 2004

Pageable DisplayTag with Hibernate

啊~~~~~~ 昨、今兩天都跟 DisplayTag 大戰。搞了好久總算搞定了。DisplayTag 這個 taglib 寫的真很不錯,介面很好看,而且 pageable, sort, group 使用上都很簡單。最後還有無限變化的 decorator,設計的人真的非常有經驗!然而它還是有一些地方還不夠完美。使用它的 page 功能,比方說120筆分12頁,每頁10筆好了,它沒有機制可以讓你一次只讀10筆。一定要一次讀完 120 筆才行。這樣分頁的用意就少了一大半了!這兩天就都花在這玩意上,浪費了好多時間。最後的答案還算差強人意。

HibernatePageList page = new HibernatePageList(pageSize, "from Foo f where f.id >20" ) ;
page.loadPage(2) ;

HibernatePageList 是一個帶有 iterator() method 的 list (不全是,沒有 implement List) 。這個物件裡面會有空的 list,裡面全部裝滿 null,整個 list 的大小就是 上面 "from... >20" query 出來的筆數。當執行 loadPage(2) 時,內部會去抽第二頁所需的資料,然後 HibernatePageList這個物件的內部的List 的第二頁會被填滿(其餘保持 null) 。如此當使用者點到分頁的第二頁時,在 servlet/action 裡執行 loadPage(2),就可以讀取需要顯示的值。

displayTag 本身並沒有提供讀取目前是第幾個分頁的功能。我們只好從它的 request parameter 下手;當 displayTag 分頁時,比如說第三頁,它會在 url 後面接上 "&d-12345-p=3" 這樣的 parameter 。12345 是它產生的隨機碼,有時是四個數字。抓取這個數字可用 regular expression : params.match("d-[0-9]+-p") 這個 pattern 來找,找到後便可傳給 loadPage. 所以最後可簡化為:

page.loadPage(request) ; // get current pageNo from request

完整的程式碼如下:

public class HibernatePageList {

private List cachedList;
private int pageNo;
private int pageSize;
private int totalSize;
private String queryString;

public HibernatePageList(int pageSize, int totalSize, String queryString) {

this.queryString = queryString;
this.pageSize = pageSize;
this.totalSize = totalSize;

// initialize full list with null;
cachedList = new ArrayList(totalSize);
for (int i = 0; i < totalSize; i++) {
cachedList.add(null);
}
}
public HibernatePageList(int pageSize, String fromBasedQueryString)
throws HibernateException {
this(
pageSize,
getTotalSize(fromBasedQueryString),
fromBasedQueryString);
}
private static int getTotalSize(String fromBasedQueryString)
throws HibernateException {

return (
(Integer) HibernateUtil
.currentSession()
.iterate("select count(*) " + fromBasedQueryString)
.next())
.intValue();
}
public void loadPage(final int pageNo) throws HibernateException {
//check cache has data:
if (pageIsCached(pageNo)) {
return;
}
Query query = HibernateUtil.currentSession().createQuery(queryString);
query.setMaxResults(pageSize);
query.setFirstResult(getPageStartRow(pageNo));

final Iterator iter = query.iterate();
for (int i = getPageStartRow(pageNo); iter.hasNext(); i++) {
cachedList.set(i, iter.next());
}
}

public void loadPage(HttpServletRequest request)
throws HibernateException {
loadPage(getDisplayTagPageNo(request));
}

/**
* extract page no from request of display tag
*
* @param request
* @return
* @throws NumberFormatException
*/
public int getDisplayTagPageNo(HttpServletRequest request)
throws NumberFormatException {
Enumeration names = request.getParameterNames();
int pageNo = 1;
while (names.hasMoreElements()) {
String parameter = (String) names.nextElement();
if (parameter.matches("d-[0-9]+-p")) {
pageNo = Integer.parseInt(request.getParameter(parameter));
break;
}
}
return pageNo;
}
private int getPageEndRow(int pageNo) {
return Math.min(pageNo * pageSize, totalSize);
}
private int getPageStartRow(int pageNo) {
return (pageNo - 1) * pageSize;
}

private boolean pageIsCached(final int pageNo) {
boolean pageIsCached = true;
for (int i = getPageStartRow(pageNo);
i < getPageEndRow(pageNo) && pageIsCached;
i++) {
if (cachedList.get(i) == null) {
pageIsCached = false;
}
}
return pageIsCached;
}
public Object get(int index) {
return cachedList.get(index);
}
public boolean isEmpty() {
return cachedList.isEmpty();
}
public Iterator iterator() {
return cachedList.iterator();
}
public int size() {
return cachedList.size();
}
public int getPageSize() {
return pageSize;
}
public String getQueryString() {
return queryString;
}
}

星期二, 4月 27, 2004

How to apply test-first metholody for unknown ?

這句話今天一直在腦海中響著。早上一開始就跟 servlet 的檔案下載奮鬥,這玩意寫了好幾遍了。最後一次寫是用 servlet 產生 JFreeChart 的圖。不過每次都忘得一乾二淨,尤其這次的下載,檔名也是 requirement 之一。最後東翻西翻想到的作法是:

//Foo is a utility to convert request parameter to object.
ServletDownloadable download = Foo.build(request);
response.setHeader("Content-Disposition",
      "attachment; filename=" + download.getFileName());
response.setContentType(download.getContentType());
download.write(response.getOutputStream());

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 嗎?)

星期一, 4月 26, 2004

Immutable Object and Test-Driven Development

今天又有新發現!原本是想寫一個 number range 的 parser, 例如 使用者輸入 "128~130,140" 時,parser 會回傳 128,129,130,140 四個數字。直覺的寫法就是寫個 parser 的工具:

NumberRangeParser parser = new NumberRangeParser() ;
List results = parser.parse("128~130,140");

後來想說直接將 "128~130,140" 直接轉成 SQL: (foo between 128 and 130) or ( foo = 140)
啊咧?怎麼好像又要多做一個工,而且還要多寫一個 converter。那API 不就變的很醜,而且很囉嗦..... 不過似乎沒什麼選擇,還是寫吧。嘿!來試試 Test-Driven Development,先寫個 test 再來寫 code。

......怪怪,這樣不好,嗯.... 這樣改比較好.... 喔,這邊可以寫成這樣....

經過一連串的 design refactoring ( code 還沒寫 ) ,最後 API 有了驚人的轉變:

NumberRange numberRange = new NumberRange("128~130,140") ;
List numbers = numberRange.toNumberList();
String SQL = numberRange.toSQL(String foo);


Whoa ! A range of numbers become an immutable Object !

在原本的設計裡 parser 只是一個 tools 而已,這就像是寫 procedure code 的 subroutine 一樣,原本的寫法就算是全寫成 static 照樣可以運作。新的寫法則完全是物件的思維,那一串的 number range 整個變成物件,然後可以轉換成各種格式。而且以後在每個 class 之間使用時,都是直接傳 NumberRange ,而不是用 number 的 List 來傳遞,不僅僅防呆,而且界面的彈性更好!最後這個 class 是設計成 immutable 的,換句話說,這個 class test 完後,就永遠不會再錯啦!WOW!而且這個 class 感覺好像 String class 喔,心中突然覺得很爽,自己也能寫出類似大師的程式!

現在還不清楚這是不是 test-first 帶來的好處。嘿嘿!明天再來試試!

星期日, 4月 25, 2004

Some experiences with unit test...... and Object Mother Pattern

之前 JUnit in action 唸了半本,當時想想這樣開發程式真是完美啊!所有的 component 都是用 interface 隔的一乾二淨的。然後每個 component 跟其他的 component 之間 都是用 IOC (Inversion of Control) 設定。這樣開發的時候就可以用一堆 mock,而不用再準備一大堆的 test fixture。起初唸完還興奮的不得了說....... 不過直到最近..... 開始真正使用 Junit 之後,惡夢才真的開始!

惡夢中體會到的:

  • Not all components need to apply interface ! 如果幻想每個 component 都用 interface.... 那就會有管不完的 interfaces !不暈倒才怪!應該是只有環境 (例如 file system, connection.... etc) 以及各子系統間才需要額外添加 interface.
  • For mass test fixture, Use Object Mother Pattern ! 這個 pattern 真是棒!不僅可以管理/重用辛辛苦苦建立的 test fixture ,也可以有效的清掉(teardown)建立的 fixture. 而且發現搭配 Hibernate 後使用起來更是爽!
  • I don't write any code today !! 終於!我也犯了全天下 unit tester 都會犯的錯。居然一整天都在 write/refactor test case, fixture, 還有那個 Object 媽媽...... 媽媽咪啊!Business code 一行也沒寫到!

A Blog Day

測試 blog......
從小就沒有寫日記的習慣....... 嘿!看看可以寫幾篇!