星期日, 6月 20, 2004

Hibernate Rocks

Hibernate in action 第五章出了,講的是 cache, concurrency 和 transaction,拜讀之後真是對 Hibernate 五體投地,什麼事都幫你設想好,只要在 hbm 設一設就搞定了。真的是 transparent persistent 無所不在。Hibernate in action 也提供了一些 concurrency guideline,不然那個什麼 read uncommited / read commited / serializable.... etc 真的很繁鎖,而且又難。這對我們這種半調子 programmer 真的算的上是天上掉下來的禮物。用 Hibernate:

(1) making difficult work extremely easy, making impossible task possible.
(2) 用慣了 Hibernate 會把你變成 persistent 白吃 :-P
(3) A true vendor lock-in ! 因為無法想像沒有 Hibernate 要怎麼開發~~ 呼呼

星期四, 6月 17, 2004

Test infected

Test infected, 這個是 Test 大師 Kent Beck 形容 "非寫 test,不然就沒辦法 coding" 的一種習慣。這個字該怎麼翻好呢? "被測試感染"、"感染到測試"、"後天性測試症候群"、"急性測試炎".... -_-;; 好像太離譜了~~ 還是等待大師來翻譯正名吧.

Anyway, 這個禮拜又玩了一下 test-first 理論。這次從兩個方向著手,第一個是 service oriented 的程式。service 嘛,因為只知道它要做什麼,怎麼個做法還不知道,所以自然是從上到下 (top-down) 的方式來開發,先從最外面的輪廓著手:


//[code snip#1]
public class DataService {
   public void sendData(String data) {;}
}

(note: test 比這個早寫完)
接下來開始工作讓這個 service 工作,這個 service 會送資料到某個檔案,所以就變成這樣:


//[code snip#2 心中閃過的念頭,假的]
public class FileDataService {
  public FileDataService(String fileName) {
    this.fileName = fileName ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    new FileWriter(fileName).write(.....)
    //etc
  }
}

其實這個 FileDataService 並沒有真的寫出來,因為先寫 test 就知道 ==> 有夠難測!為了要 assert 這個程式的結果,我還要去找個真的檔案來寫,寫完後再來個 FileReader,讀完了之後,再把這個檔案給 purge 掉,暈倒... 為了讓 test 好寫,就將 fileName 拿掉,改用 OutputStream 當成介面:

//[code snip#3 test寫完後的第二個版本]
public class DataService {
  public DataService(OutputStream out) {
    this.out = out ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    out.write(...);
    //etc
  }
}

呼呼,這下 output 的來源被丟出去了,改成從 constructor 進來,這就叫 constructor injection 吧?測試這個程式就簡單多啦,我可以 new ByteArrayOutputStream() 給它,然後就.... 嘿,這個留給看倌自己測試啦,這個 class 很好用地~~ (hint: toByteArray() )。

後來這個程式演變成:
//[code snip#4 第三個版本]
public class DataService {
  public DataService(OutputFactory factory) {
    this.factory = factory ;
    //etc....
  }
  public void sendData(String data) {
    String newFormat = formatConversion(data);
    OutputStream out = factory.createOutputStream();
    out.write(...);
    //etc
  }
}

用一個factory來建立新的 OutputStream,因為這樣這個 service 才能送資料到不同地方,而這個 DataService 在進行測試時,其實 OutputFactory 還沒寫好,只是使用一個 dummy 的 MockOutputFactory 來輔助測試,DataService 才不管要寫到哪裡去咧,這是 OutputFactory 的事。它專心做它該做的事 (data conversion, write data... etc) 。由上面這個例子可知 test-first 帶來了: 逼你去想好測試的方法,把不相干的東西盡量丟到外部去,唯有這樣 Object Isolation 才會好。

Ok, 現在進入第二個例子:bottom-up,就是由小到大,從細部做起:我想設計一個 TableModel,有 cell, 也有 row,然後可以定義 cell 的 format,而且還有一個特殊功能,可以合併 table。由小到大,先寫 cell 、再寫 row、最後寫 Table

//[code snip#5 由小到大]
public class DataCell {
  public DataCell(Double data, DecimalFormat format) {;}
}
public class DataRow {
  public DataRow(int initialSize) {;}
  public void addCell(DataCell cell);
  public void appendRow(DataRow rowToCombine);
}

上面是寫完的結果,步驟是這樣地:首先先寫 DataCell 的 test,再完成 DataCell 本身 (DataCell設計成 immutable) 。有了 DataCell 之後,就開始寫 DataRow 的 test,這個 test 就好寫了,因為已經有"測試完整"的 DataCell 可用,不用費心去做 Mock。而那個特別的 appendRow() method,只做了個大概,這是想到等一下要用來合併 Table的 "原料",也許用不著也說不定。有了 DataRow 就可以拿來寫 DataTable 了:

//[code snip#6 完成 DataTable]
public class DataTable {
  public DataTable(int initialRow, int initialColumn) {;}
  public void addRow(DataRow row);
  public void appendTable(DataTable tableToCombine);
}

同樣,DataTable 因為有 DataRow 可用,所以寫來很快。而那個 appendTable 在測試的時候,不斷的去 refactor DataRowTest 和 DataRow 的 appendRow() method,以符合 DataTable 的需求 (比如說同樣的 column 數才能合併之類的條件)

從小到大的撰寫,正常來講除非設計的很完善,知道每個小元件該做什麼,才比較好實施,不然常常寫到大的部份才要重翻會瘋掉... 通常 "常識"之類的設計需求,例如本例中的 Table,大多數的狀況 Table 都是由 row 和 cell 組成的;又或者是車子,大部份都有 engine、輪胎等 parts ... 像這樣子的比較適合吧,因為這種架構都很固定了。

回到正題.... test-first 在這裡也發揮了功能: 我寫完這 DataCell, DataRow, DataTable 時,完全沒有做任何的 debug ! 每一個 class 都是建立在完整測試好的子元件上。當 DataRow 的 appendRow() 不夠 Table的 appendTable 用時,先增加/修改 DataRowTest 的 assertion,然後再修改 DataRow 直到綠燈亮為止。這樣確保 appendTable 去call appendRow時一定不會出錯,也就是撰寫 test 永遠比 refactoring 先做。我雖然不知道如果沒寫 test 的話會發生什麼奇怪的臭蟲,不過這種不用 debug 就寫完程式,這可是寫程式以來還沒遇過的事啊~~~~ 而且完成度還很高 (有一堆 test 掛保證 :-)

不論是 bottom-up, top-down, test first 都有地方可以發揮功能。top-down 需要用點小技巧來做 isolation,通常寫完之後會留下一堆 mock 或 dummy class ,而 bottom-up 比較不會,但是需要事先良好的設計。而且等到子元件越疊越大時,還是得做 mock,不然建立 fixture 的時候會吐血 (也可以用 Object Mother pattern 來解決啦)

Conclusion.... I have been test infected !

星期一, 6月 07, 2004

強迫性 Immutable 症候群

自從看了 Domain-Driven Design 這本書之後,唉~ 不知該說好還是壞。就說裡面提到的 Side-effect free function 吧。side-effect free指的是 object 是 immutable 的,做了改變不會影響到本身也不會氾濫到其他的 object,而 function 則是指這個 method 可以執行多次而結果都一樣 (數學的 function 都是這樣)
直覺式的錢包計算:

//[code snip #1]
public class MoneyBag {
  private int asset ;
  public void spend(int expenditure) {
    asset -= expenditure;
  }
}

這樣夠嗎?當然不夠,還要加 precodition 和 post-condition:

//[code snip #2]
public class MoneyBag {
  private int asset ;
  public void spend(int expenditure) {
    if(expenditure <= 0)
      throw new IllegalArgumentException();
    final int diff = asset - expenditure;
    if(diff < 0){
      throw new InsuffienctMoneyException();
    }
    asset = diff;
  }
}

夠醜吧,真正的 logic 只有一行,卻弄成啊哩啊扎的
如果另外寫個 Immutable Value Object "Money":

//[code snip #3]
public class Money {
  private int dollar ;
  public Money(int dollar) {
    if(dollar <= 0)
     throw new IllegalArgumentException();
    this.dollar = dollar ;
  }
  public Money minus(Money money) {
    int diff = this.dollar - money.dollar ;
    if(diff < 0){
     throw new InsuffienctMoneyException();
    }
    return new Money(diff) ;
  }
}

MoneyBag 就不用再煩惱 "錢" 的問題了,變得很簡捷:

//[code snip #4]
public class MoneyBag {
  private Money asset;
  public void spend(Money expenditure) {
    asset = asset.minus(expenditure);
  }
}

spend() 變得超直覺的 ==> "剩下的錢等於現在的錢 minus 消費額"
再看另一個例子:

//[code snip #5]
public Money calculateDiscount(Money expenditure) {
  Money allowance = expenditure.multply(0.9) ;
  return expenditure.minus(allowance);
}

不會有人看不懂這個method在做什麼吧.... 已經非常非常白話了。
  • minus() 這個 method 重用了
     
  • 隨便你怎麼 call、怎麼組合、call多少次都不會錯
     
  • 核心邏輯可讀性很好,接手維護的人會感動到哭

這些優點都是歸功於 Side-effect free function 的設計理念。真是棒呆了...... 呃... 事情若這麼順利就好了。自從看完這個章節後.... 我的眼中只容的下 Immutable Object ! 哇咧~~ 一看到有什麼 method 不是 immutable 的,就想做 refactoring。再加上功力又不太夠,沒辦法很正確的判斷,搞得 原來的 class 一團亂 -_-;;。而且,本來嘛,那個 MoneyBag 是一個 class 就搞定了,現在要多生一個 Money class 出來,Immutable 好像天生就很容易增加 class 數量的樣子... 這樣到後來也不怎麼好維護吧?!

唉~ 不知該說好還是壞~~