0.2版在修正了0.1版錯誤的基礎新增了驗證碼組件、DomainEvent框架和工具類。地址是:[url]http://code.google.com/p/basicaidedcomponent/[/url],歡迎大家前往下載試用。同時我在Google註冊了一個論壇[url]http://groups.google.com/group/bac_china/[/url],不過訪問總是時靈時不靈的。
說完了框架發佈,我在這裏談一下0.2中比較特別的DomainEvent框架。這也是希望大家不要投新手帖。在這裏發佈自有框架,總會被貼上幾個新手帖。一個弄不好就被丟到新手區了。
DomainEvent框架是爲了解決實體與服務的交互而提出來的。在我們日常開發中,領域對象和Service的交互有時候需要領域對象調用Service。
比如我們有一個班級類,班級裏有List保存學生對象。現在要求有一個統計學生的功能。如果完全按照領域驅動設計,這個功能應該是班級類自己的工作。如果所有的對象都在內存中,倒也沒什麼,直接統計一下List裏學生對象數量就是。
public class Clazz {
//學生
List students;
public int getStudentSize() {
return students.size();
}
}
但是實際上這些對象都是保存在數據庫中的。我們需要使用ORM工具獲取Clazz,而一般學生列表是延遲加載的。在讀取students列表時,ORM再加載學生實例。如果我們需要學生列表數據也罷了,但是如果我們只是要一個學生數量,那麼將所有的學生對象加載就是非常不合算的事情。也許有人說寫查詢語句直接讓數據庫統計一下就是,比如下面:
select count(s) from student s where s.clazz.id = ?
但是問題是如何調用呢?總不能寫成下面這種代碼
public class Clazz {
//學生
List students;
//一個學生Service對象,用於處理學生相關業務
StudentService service;
public int getStudentSize() {
return service.getStudentSize(this.id);
}
}
很明顯,實體和服務產生了緊耦合。先不說我們如何把StudentService注入Clazz的實例中,就是可以注入,產生的緊耦合也使得Clazz和StudentService綁到了一塊。如果寫成單獨的Dao或者Service方法,那麼就可能造成Clazz只是一個純粹的持久化對象,沒有了任何業務。
2008年Udi Dahan在其博客[url=http://www.udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/]How to create fully encapsulated Domain Models[/url]一文中也提出這個問題。一個實體模型中的方法參數依賴服務或者Repository,那麼就會造成實體和服務的緊耦合。
2009年6月14日作者在徵詢很多意見後,再次在其博客[url=http://www.udidahan.com/2009/06/14/domain-events-salvation]Domain Events – Salvation[/url]提出了Domain Event的解決方案。採用了事件/消息模型對實體和服務進行解耦。
定義一個事件接口
public interface DomainEvent<T> {
/**
* 獲得發生Event的對象
* @return Event的對象
* @since 0.2
*/
public T getSource();
/**
* 獲得Event類型
* @return Event類型
* @since 0.2
*/
public String getType();
}
然後定義監聽器接口
public interface DomainListener {
/**
* 處理事件
* @param event 事件
* @since 0.2
*/
public void handler(DomainEvent event);
}
現在我們可以實現一個Listenrer,把查詢學生人數的代碼放到監聽器裏。
public class StudentListener implements DomainListener {
StudentService service;
public void handler(DomainEvent event) {
if (event.getType().equlas("getStudentSize") {
int size = service.getStudentSize(event.getSource().getId());
event.getSource().setStudentSize(size);
}
}
}
然後Clazz只要在需要查詢人數的時候發出一個事件。
public class CLazz{
int studentSize;
public void getStudentSize() {
DomainEventManager.disptcher(new DefaultDomainEvent(this, "getStudentSize"));
}
}
//一個簡單的DomainEvent實現
public class DefaultDomainEvent implements DomainEvent {
private Object object;
private String type;
public DefaultDomainEvent(Object object, String type) {
this.object = object;
this.type = type;
this.sync = sync;
}
public Object getSource() {
return object;
}
public String getType() {
return type;
}
}
監聽器在收到事件後就會自動調用代碼進行查詢,並把查詢結果放到Clazz中。這裏只進行一些簡單描述。大家想看完整的DomainEvent代碼,可以下載我的BAC框架。
disptcher會將事件交給事件管理器,事件管理器是一個Command模式,它調用Listener對事件進行處理。
public class DomainEventManager{
/**
* 已註冊的監聽器集合
*/
private final static Map<Class, List<DomainListener>> listenerMap = new ConcurrentHashMap<Class, List<DomainListener>>();
/**
* 處理事件
* @param event
* @since 0.2
*/
public static void dispatch(DomainEvent event) {
//如果存在該類的監聽器
if (hasClassListener(event.getSource().getClass())) {
List<DomainListener> listenerList = getClassListeners(event.getSource().getClass());
//循環監聽器,這是一個典型的Command模式
for (DomainListener listener : listenerList) {
listener.handler(event);
}
}
}
}
這樣實體和服務就分開了。不會造成實體和服務的緊耦合。