星期四, 7月 22, 2004

My favorite usage of Struts ActionForm

First ! my English is very bad.... so... you know what I mean !

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...)

public class EmailForm extends ActionForm {
private String emailDisplay ;
public String getEmailDisplay() {...}
public void setEmailDisplay(String email) {...}
public Email getEmail() {
return new Email(getEmailDisplay());
}
public void reset(
ActionMapping mapping,
HttpServletRequest request) {
setEmailDisplay(null);
}
public ActionErrors validate(
ActionMapping mapping,
HttpServletRequest request) {
//...
EmailValidate.check(getEmailDisplay());
//...
}
}

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:

public class UserForm extends ActionForm {
private EmailForm emailForm = new EmailForm();
private String userName ;

public String getEmailForm() {...}
public void setEmailForm(EmailForm emailForm) {...}

public Email getContactEmail() {
return getEmailForm().getEmail();
}
public void reset(
ActionMapping mapping,
HttpServletRequest request) {
setUserName(null);
getEmailForm().reset(mapping,request); // reuse reset()
}
public ActionErrors validate(
ActionMapping mapping,
HttpServletRequest request) {
//reuse validate()
ActionErrors errors
= getEmailForm().validate(mapping, request);
// do rest of validation.
UserName.check(getUserName());
}
public String getUserName() {...}
public void setUserName(String userName) {...}
}

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:

<html:form ... >
User Name: <html:text property="userName" />
Contact Email: <html:text property="emailForm.emailDisplay" />
</html:form>

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:

public class CRUDForm extends ActionForm {
private String createButton ;
private String readButton ;
private String updateButton ;
private String deleteButton ;
public boolean isCreate() {
return isButtonPressed(getCreateButton());
}
public boolean isRead() {
return isButtonPressed(getReadButton());
}
public boolean isUpdate() {
return isButtonPressed(getUpdateButton());
}
public boolean isDelete() {
return isButtonPressed(getDeleteButton());
}
private boolean isButtonPressed(String button) {
return button != null

|| button.trim.length() > 0
}

// getter/setter of createButton, readButton,....etc.
}

Above create an elementory CRUD (create/read/update/delete) ActionForm, we can apply it to our UserForm too.

public class UserForm extends ActionForm {
private CRUDForm operation = new CRUDForm();
public void setOperation(CRUDForm operation) {...}
public CRUDForm getOperation() {...}
// rest of fields like userName, email... etc
}

while in Action, write some codes like below:

public ActionForward execute(...) {
UserForm userForm = (UserForm) form;
if(userForm.getOperation().isCreate()) {
doCreateUser();
} else if (userForm.getOperation().isRead() {
doQueryUser();
} else ....
// rest of doXXXUser()....
}

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.

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.

星期日, 7月 18, 2004

好文分享:獨孤木專欄 - Process

看看連結吧:
爪哇教室> 獨孤木專欄 - Process《上》
爪哇教室> 獨孤木專欄 - Process《下》

寫的真是好啊,舉的例子都是一針見血,都是在我們週遭發生的呀(非資訊業也是如此)。我個人對 process 沒什麼經驗,但是卻很痛恨文件。所以一直很想在 team 裡面推 XP,最少這是文件最少的 process 了。我們自個的 team 雖小,十個人吧,大概不會有大企業的那種阻力,但是其中也是有人獨尊 UML,希望多畫一點... 哈,看來衝突在所難免!

不過看了 singlelog 的文章後,真的要三思後行啊... 也許真的沒有那種 super process 可以套用所有的專案,目前比較可行的是 "量身訂作" 吧?

嗯,再想想,再多討論一點吧
反正我不是做決策的人 ! 輪不到我做決定啊~~~~~

Object Mother: Automatic register Hibernate persistent object for tear down

寫過 Unit Test 的開發人員都曾經為了建置 fixture 而傷透腦筋吧?,尤其是準備資料庫的資料來滿足各式各樣的測試條件時更是頭痛。Object Mother Pattern 就是專為建立複雜的fixture而誕生的,先來看看一個簡單的 Object Mother 吧:

public class UserObjectMother {
public static User createNonActiveUser() throws Exception {
return new User("anonymous"); // default non-active
}
public static User createActiveUser() throws Exception {
User anonymous = createNonActiveUser();
anonymous.setActivate(true);
return anonymous ;
}
}

首先,由 createNonActiveUser() 這個 method 可以建立一個未啟用的使用者,接下來的 createActiveUser() 則是先呼叫 createNonAciveUser(),重新設定為啟用的使用者後傳出去。因此,我們可以用UserObjectMother 建立不同條件的使用者以供測試使用。而且我們可以發現 createActiveUser() 重用了 createNonActiveUser() 這個 method。 依這樣的設計延伸,我們可以再寫個 ProejctObjectMother:

public class ProjectObjectMother {
public static Project createProject(String projectName)
throws Exception {
Project prj = new Project(projectName);
prj.setOwner(UserObjectMother.createActiveUser()) ;
return prj ;
}
public static Project createProjectWithMembers()
throws Exception {
Project prj = createProject("anonymous prj");
User[] members = new User[] {
UserObjectMother.createActiveUser(),
UserObjectMother.createActiveUser(),
UserObjectMother.createActiveUser()
};
prj.addAllMembers(members);
return prj;
}
}

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() 裡被呼叫)。

OK, 接下來就是重頭戲了:tear down。為了保持 test isolation,我們必須 run 完一個 test case 就 清掉所有產生的物件。從上面的例子看來,我們不需要做任何的清理動作。程式結束後自然會被 garbage collect 掉。比較麻煩的是資料庫裡的持久化資料,因為不做清理的話,會一直影響其他的測試。很多做法是建立一個 insert 的 SQL script,配另一個 delete/drop 的 SQL script。這裡我們採用 Hibernate 的方式來產生資料庫的物件,一方面可重用原來的 createXxx 的 method,另一方面 refactoring 時才不用維護兩套系統。

public class UserObjectMother {

// other method like createNonActiveUser()
// and createActiveUser() ....

public static User savedActiveUser() throws Exception {
User activedUser = createActiveUser();
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction;
session.save(activedUser);
tx.commit();
return activedUser;
}
}

這裡的 HibernateUtil 是 Thread local pattern 的產物,Hibernate 的網頁有介紹怎麼使用。如果你對這種寫法不熟,請先停止閱讀,先到 Hibernate 網頁一探究竟 (很值得學的) 。Anyway, savedActiveUser() 這個 method 重用了 createActiveUser(),並且開啟 transaction 將資料寫入資料庫裡,然後回傳一個 persistent object(PO)出去。(注意,本文中的 User, Project... 等等 都是 Hibernate 的 PO) 如此,test case 便能測試和資料庫相關的功能。好了,接下來我們要清理掉這個存過的物件。最簡單的做法就是自己寫:

public class UserTest extends TestCase {
User anonymous ;
public void setup() throws Exception {
anonymous = UserObjectMother.savedActiveUser();
}
public void tearDown() {
// begin transaction code.
session.delete(anonymous);
// commit transaction code.
}
public void testActiveUserChangeProject() {
// do some business test...
}
}

在tearDown()裡,我們用 delete() 來清理PO。如果這麼簡單,就不用 pattern 來解決囉。當 UserOM, ProjectOM, OrderOM 不斷產生新的 PO 之後,tear down 將複雜到完全無法撰寫。這時我們要進入 Object Mother pattern 的核心 -- 註冊那些新儲存的 PO,然後到後面再一口氣清掉。我們先來看看在概念上是怎麼用的吧:


public class UserTest extends TestCase {
User anonymous ;
Project newProject ;
public void setup() throws Exception {
anonymous = UserObjectMother.savedActiveUser();

// savedProject() call createProject
// then save into database.
newProject
= ProjectObjectMother.savedProject("new proejct");
}
public void tearDown() {
// purge all saved PO, including "anonymous"
// and "newProject"
RegisteredObject.getInstance().purge();
}
public void testActiveUserChangeProject() {
anonymous.changeProejct(newProject);
// do some business test...
}
}

tearDown() 只變一行了,而且不論我們用多少種 UserOM, ProjectOM... ,都只要call purge() 一次就可以清掉。我們來看看怎麼實做吧,首先,要達到 call 一次 purge() 就將所有的 PO 都清除掉,我們需要一個 singleton:

public class RegisteredObject {

private static final RegisteredObject instance
= new RegisteredObject();

private RegisteredObject() {}
private static RegisteredObject getInstance() {
return instance;
}
private List allRegisteredPOs = new ArrayList();
public void add(Object entity) {
allRegisteredPOs.add(entity);
}
public void purge() throws Exception {
HibernateUtil.closeSession(); // this step is important.
if(allRegisteredPOs.isEmpty()) {
return ;
}
Transaction tx = null;
try {
Session session = HibernateUtil.currentSession();
tx = session.beginTransaction;

// FILO, first in last out to delete PO
for(int i = allRegisteredPOs.size()-1;i >= 0;i--) {
session.delete(allRegisteredPOs.get(i));
}

tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
allRegisteredPOs.clear();
HibernateUtil.closeSession();
}
}
}

我們可以看到 RegisteredObject 是個 singleton 而且裡面有個 List allRegisteredPOs 來儲存所有註冊的 PO。然後在 purge() 裡面將所有註冊的 PO 全部清掉。purge 很長,我們等一會再看,我們先看看在 Hibernate 中要如何註冊新儲存過的 PO:

public class RegisterInterceptor implements Interceptor {
private RegisteredObject registeredObject;

public RegisterInterceptor(RegisteredObject reg) {
this.registeredObject = reg ;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types)
throws CallbackException {

registeredObject.add(entity);
return false;
}
// rest of methods: like onLoad(), onDelete().... etc.
}

RegisterInterceptor,一個 Hibernate 的 Interceptor(攔截器)。Hibernate 的攔截器 可以對 persistent 的生命週期做一些特殊的處理,功能有點像是 database 的 trigger。以這裡的 RegisterInterceptor來說,在 PO save 前的一刻,它會呼叫 onSave()這個 method。我們利用這點將該 entity 註冊到 registeredObject 裡,如此一來就可以獲得所有新儲存的 PO 了。有了 Interceptor,下一步就是安裝在 Hibernate 的 session 裡,這樣子只要是透過 session 儲存的 PO 都會被我們攔截下來並註冊。

public class HibernateUtil {

private static Interceptor interceptor ;
public static setInterceptor(Interceptor i) {
interceptor = i;
}

private static final ThreadLocal threadLocalSession
= new ThreadLocal();

public static Session currentSession()
throws HibernateException {
try {
Session s = (Session) threadLocalSession.get();
// Open a new Session, if this Thread has none yet
if (s == null) {
// add interceptor if it has one.
if (interceptor == null) {
s = sessionFactory.openSession();
} else {
s = sessionFactory.openSession(interceptor);
}
threadLocalSession.set(s);
}
return s;
} catch (HibernateException e) {
logger.fatal("error in getting a session", e);
throw e;
}
}
}

上面是 HibernateUtil 的一小部份。我們加了 setInterceptor() 的 method,讓我們從外部設定 interceptor。而 currentSession() 裡,我們多了sessionFactory.openSession(interceptor); 這一行。所以只要我們設定 interceptor 之後,HibernateUitl.currentSession() 所回傳的 Hibernate session 都會加裝攔截器,在這裡我們當然會設定為 RegisterInterceptor 囉!我們在看看改寫過的 UserTest:

public class UserTest extends TestCase {
User anonymous ;
Project newProject ;
public void setup() throws Exception {
// create interceptor
RegisiterInterceptor interceptor
= new RegisiterInterceptor(
RegisteredObject.getInstance());

// setup interceptor for all created session
HibernateUtil.setInterceptor(interceptor);

anonymous = UserObjectMother.savedActiveUser();
newProject =
ProjectObjectMother.savedProject("new proejct");
}
public void tearDown() {
RegisteredObject.getInstance().purge();
}
}

setup 裡多了兩行,一行是 new 一個 interceptor (用 singleton RegisteredObject 當容器),第二行是將interceptor設在 HibernateUtil 裡。如此一來,所有經由 HibernateUtil 拿到的 session 都會對儲存的物件註冊;而到了 tearDown(),再執行 RegisteredObject 的 purge() 就可以全部清掉了。好,現在回過頭來看 purge() 是怎麼寫的。

首先 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() 掉。

第二個就是 delete 的順序。在這裡我們採用 FILO 先進後出的順序來 delete() PO。這個是與 Database 的 foriegn key (FK) 有關。當 insert 資料到 database 時,一定是depend 最多 FK 的物件先 insert ,才會輪到 FK 物件 insert,依此類推,最後 insert 的物件是完全不會有被 FK 指向到的,因此這樣的物件是可以先 delete 掉,而不會有 FK 衝突。因此我們清除的順序是先將後到的 PO delete,再漸漸的往前推,直到最早 save 的那一個物件為止。

第三個是 finally 裡面的 allRegisteredPOs.clear(); 因為 RegisteredObject 是 singleton ,我們每次執行 purge 之後就要將註冊的內容清除,不然一個個 test case 執行下去會讓 allRegisteredPOs 越來越大,也不合理。

最後,這裡還有一個隱含的功能:按 FILO 的順序是沒辦法正常清除掉可以 null 的 FK 關係的。因為他們兩者都可以先獨立儲存 (不分順序) ,然後再連成 FK 的關係,這樣一來按 FILO 刪除就沒意義了,因為可能要先移除的反而先儲存,順序就錯了。好在 Hibernate 的 "Transactional write behind" 可以輕鬆解決這個問題。只要在同一 transaction 裡,Hibernate 會知道誰要先清掉,然後重新安排 delete 的順序。也許你會說那不就不用 FILO 了嗎? FIFO 就可以了啊.... etc。這裡我只能說,經實驗證明,是不可行的。原因是什麼我也不清楚。也許是 Hibernate 內部的排序上有極限,又有可能是即使 PO 間有互相 FK 的資訊,理論上也是不可能完全到推出當初儲存的順序,後者比較有可能吧.... 我想。

Summary:
這篇我們實作了二個簡單的 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 !

note:
RegisteredObject, RegisterInterceptor 兩者是 ObjectMother 該有的功能,全部整合在一個 BaseObjectMother 裡,然後讓 UserObjectMother, ProjectObjectMother... etc 去繼承它,這樣才算是完成了 Object Mother Pattern,這個留給讀者回去自己實作吧。

星期四, 7月 15, 2004

Object Mother Pattern 優缺點

最近有人提到 Object Mother ,嗯,寫一下近來的發現吧

先講講缺點:

  • Object Mother (OM) 的理念是可以註冊要清理掉的物件,以便在 tear down 的時候可以全部 purge。其實這個理念並不好 implement,目前也沒看過有什麼 framework 可以幫的上忙的。我自己的話多是用在 database相關的物件生成,一方面是 DB 的資料準備很複雜,另一方面是 Hibernate 的 session.delete(object) 在 purge 的時候幫了大忙。即使如此,註冊 object 還是要手動進行。手續很繁瑣...


  • OM 和 test code 本身 "距離太遠" ,造成 test code 閱讀困難。距離就是 test code 和 OM 之間相隔的 method和class 數。當 developer 看到 test code 時,他還要額外的去翻那個 OM 出來看,才知道到底產生的物件長什麼樣子,横跨兩至三個class,太遠了。如果 OM 又呼叫其他的 OM,那距離又更遠了,可讀性變更差...


  • 距離遠帶來的不僅是程式易讀性的問題,還會造成 test 過慢。我曾經實做過 Fixture 最大有 call 到三層的 OM,而且每一層都會 hit database,喔~~ my God ! 才十個 test case 總共要 run 30 秒以上。


  • 上面提到像是連call好幾層的 OM,其實已經超過 unit test 的範圍,等於是在做 integration test了。而且有個嚴重的問題,就是當 OM 層層呼叫時,有時最前面的錯誤會一直累積到最後面還不會發現... 因為後面的 test case 都是一直根據前面的建立的 "範圍" 下產生的。例如最前面的資料沒有cover負值的範圍,後面的 test case 如果一直按這個 OM 產生的資料寫測試。久而久之就會出現後面的 class 沒測到負值的範圍(或者是 side effect)。這個實務上我碰過一次,當時真的是嚇呆了。因為沒想到會在最前面藏著這麼一個 bug.



嘿,換來講講優點吧:


  • 寫了這麼多 OM 發現到,其實清理物件也是 test 的項目之一啊!常常會發現如果無法正常的 tear down ,那大概是程式有哪裡忘了關 resource (例如 IO 忘了 close, 或兩個 object 同時 reference 到同一個 hibernate PO) 所以花時間寫 OM 的註冊和 purge 其實是值得的,因為 release resource 也是程式中重要的一環啊。


  • OM 在 team 裡面很好用的,只要有一個人寫出來,大家都可以用,不用每個人都需要知道怎麼正確產生 object 的各種 state。


  • 上面說到了,OM 就是迷你的 integration test,相信這付出的額外代價會有回報的。


  • 極困難測試的變成可以測!測試寫的再慢再難懂也比沒有測試強上一大截,這是最重要的啊~ :-)


有機會再寫一些 Object Mother + Hibernate 的做法吧 (其實很單純.... 沒什麼了不起....)

星期五, 7月 09, 2004

Architech and Domain

這個標題取的很大,其實不是要講這麼厲害的事。從轉行到資訊業已經過了四個月了,對我來說這是第一個接觸到開發計劃 (Project) 的進行,而且內容是偏向服務性質的。這個算是資訊業的 "本行" 吧,以前雖然寫過工程應用方面的程式,不過都是兼職的性質在寫,而且是一個人做好玩的。哪有什麼時程壓力?需求變更?現在手上這個計劃目前為止大約完成一半吧... 雖然還沒有跑完整個開發流程,現在已經有很多感想。

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 給我很大的影響。

我自己覺得真的很幸運,我們的單位選用 Hibernate,而且正當緊鑼密鼓開發的時候,就出了 Hiberante In Action 這本書。對一個 project 開發的新新手來說等於是有一個名師在指點你最好的 practice。第一次學就用最好的,而不是走偏走遠了,繞了一大圈才恍然大悟。

anyway.... 我們單位目前沒 SA / SD, programmer 要自己搞定。其實還好要自己搞,不然我一定整天跟 SA/SD吵架,而且也沒有學習的機會了,況且在時程的壓力並沒有很大的情況下,還有 "驗證" 設計問題的空間咧。話說回來,有時候像是書上還沒看到的部份 (Hibernate in Action 還沒出完, Domain Driven Design 則太厚了..) 自己會先想一些方法去實作。後來在書上看到了一樣的 implemenation 時,心裡真是爽啊!會推導出與書上一樣的結論,表示現在的自己思考回路是往正確的方向在走啊!

之前我曾經做過有關 Hiberante architech 的一些小結論。現在再整理一些東西吧:


  • Entity 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,純用來傳遞的物件)

  • Value Object (VO) VO 跟 Entity 是相對應的,它沒有 id,多與 Entity, VO 協同工作,而完成任務後,就被 garbage collect 掉了。像這樣的 class 因為物件不具識辨性,所以最好是設計成 immutable 且 side effect free (因為生成多少個都不影響功能,可以看我之前寫的 Immutable 症候群)。當它依附在 Entity 之下時,像是 user 下的 address,它也會跟著 persistent,這時在 Hibernate 裡就是用 component的方式實做。

  • Aggregate 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() 了事

  • Repository repository 就是類似 DAO 一樣的元件,方便做 CRUD 和 findByXxx()的動作。但是和傳統DAO做法不同的是,並不是一個 table 一個 DAO class,而是一個 Aggregate 一個 Repository,cover 的範圍以 domain 為單位。像是車子內的這幾個class 不會有自己的 repository ,只有 root 車子才會有 CarRepository,這個與上面的 Aggregate 相互應:root class 是對外的唯一窗口。在 domain 裡,我們不可能不找車子,而直接找第三個輪胎。如果胡亂寫個 WheelRepository 的話,不通過車子任意讓人讀取,這會造成架構的混亂,未來車子要變更設計時... 頭就很大了!anyway, 在 Hibernate 裡實作 repository 非常容易,像是儲存車子資料好了:

    public class CarRepository {
    public add(Car car) {
    session.save(car);
    }
    }

    其他的子 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 難度而已

  • Service with Domain IOC Service是統合所有流程的地方,裡面會有數個 class 協同運作,這時我們可以利用 IOC (Inverse of Control, 又稱 dependency injection) 將 CarService 設計為 :

    public class CarService {
    public CarService(CarCleanUtil cleanUtil,
    CarRepository repository,
    ChargeManager manager) {
    // some construction...
    }
    public void washCar(Long carId) {
    Car car = repository.findById(carId);
    int cost = car.washBy(cleanUtil);
    manager.charge(car.getOwner(), cost);
    repository.updateCarRecord(car);
    //.... etc
    }
    }

    一些 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相關的部份拿出去變成
        public CarService(EmailUtil emailUtil) 
    ,這樣是不錯,但是 EmailUtil 太過於 technical 了,跟 domain 比較無關。在這裡 domain的目的不外乎是為了傳達訊息給客戶,我們可以改成客戶的連絡
        public CarService(CustomerContact contact) 
    然後實作一個 EmailContact 給目前的需求使用。這樣一來以後要改用手機簡訊連絡也沒問題!


還有一個漏掉的是 Factory,到目前為止我用的還不是很多(大多是用 constructor 就搞定了)所以沒什麼心得。上述這些觀念多半是從 Domain-Driven Design 這本書學來的,經過實際與 Hibernate 並用後的一些心得。Domain-Driven Design 提到的觀念 和 Hibernate 的設計理念有很多雷同之處,因此 Hibernate 可以很簡單的 fit 這樣的架構。或者我們可以這樣說:Hibernate 幫助我們完成這樣的架構。

Again, Hibernate Rocks !

星期六, 7月 03, 2004

Dark comic

現在流行的漫畫..... 好像都那麼點黑暗。像是 ハガレン (鋼之鍊金術士) 就是講 等價交換 與 人體鍊成這種黑暗體裁。故事中不乏合成 人 + 狗 變成怪獸,或者是合成人變成異形死屍... 太黑暗了! 還有另外一部 death note (死亡筆記本) 則是死神讓人可以任意殺人... 故事雖然鬥智精彩,而且目前好像蠻賣座的... 但是.... 殺人耶! 不論故事要表達什麼,玩弄人命是要付出代價的,漫畫都畫的輕描淡寫。

現代人好像不弄點重口味的不行...