Domain Events – 救世主

在Evans DDD实现过程中,经常会碰到实体和服务Service以及Repository交互过程,这个交互过程的实现是一个难点,也是容易造成失血贫血模型的主要途径。

因为实体的业务方法需要和服务或Reposirtoy打交道,如果把这个业务方法放入服务,就容易造成实体的贫血;但是如果把服务注射到实体中,也非常丑陋。这里提出一个中间处理模式:Domain Event,领域事件模式,这个模式也曾经被MF在文章Domain Event专门章节提到。

2008年Udi Dahan在其博客How to create fully encapsulated Domain Models一文中也提出这个问题,引起大家重视。

Udi Dahan的案例是游戏购物车:对于商品放入购物车有三个规则:
1. 只有三个游戏才能加入购物车
2. 购物车中的总数不能超过10.
3. 如果该客户报失丢失了自己的租金会员,没有游戏可以被添加

前面两个规则可以在实体模型中容易实现,但是第三个条件需要和服务打交道了。


class TradeInCart{
Account Account{get;}
LineItem Add(
Game game,
IRepository<QueueHistory> repository,
LoggingService service);

ValidationResult CanAdd(
Game game,
IRepository<QueueHistory> repository,
LoggingService service);

IList<LineItems> LineItems{get;}
}

很难想象,一个实体模型中的方法参数依赖服务或者Repository?作者向大家寻求一个统一的模式来解决此一类问题。


经过近一年的讨论,2009年6月14日作者在征询很多意见后,再次在其博客Domain Events – Salvation
提出了Domain Event的解决方案,并且声称:

不要把任何东西注射到你的领域实体中,没有服务,没有仓储:
The main assertion being that you do *not* need to inject anything into your domain entities.
Not services. Not repositories. Nothing.

并且给出了Domain Event的具体实现,总体来说:就是在上面购物车实体和LoggingService服务之间引入一个事件消息模型Domain Event。关于消息事件模型我在EDA: Event-Driven Architecture事件驱动架构已经阐述:事件和消息可以说是从不同方面描述的同一个东西,消息是事件发生后产物,消息发送必须有发送事件发生才能实现。每次事件只发送一次消息,事件和消息是一对一的。

Udi Dahan的DomainEvents类底层实现实际是一个事件模式实现,采取Command模式同步机制实现的,有兴趣这可以翻墙过去看看源码,这里我提出我自己在JiveJdon主题订阅功能实现中提出的异步Domain Events模式。

JiveJdon主题订阅功能需求是这样:当用户对某个主题感兴趣,希望这个主题贴比如当前这个有新回复时通知它,他就可以使用主题订阅关注这个主题。

那么实体模型ForumThread就变成被订阅者,而用户就成为订阅者,这样,当ForumThread的业务方法addNewMessage(新回复)被调用时,立即通知订阅者。

在这个实现中,ForumThread中addNewMessage方法中增加一个通知订阅者方法就可以了,但是这个ForumThread有哪些订阅者,不可能通过聚合关系一直将ForumThread的订阅者都一次性纳入其中,这个问题在gamex帖子http://www.jdon.com/jivejdon/thread/37288
中已经提及,我们当然是采取查询的方式,但是查询就涉及数据库里哦啊,是否在ForumThread中addNewMessage方法引入Repository?

所以,我也碰到了和Udi Dahan当初一样的问题,我之前没有看过他的这篇文章,是因为刚才看到DDD: Entity Injection and Mocking Time文章才找到Udi Dahan的Doman Event模式,因为我对这个标题Entity Injection实体注射感兴趣,因为我在Jdon框架实践中,有时感觉Jdon框架不能支持实体注射(只有服务注射)而不便,也在考虑是否需要实体注射,当然,我现在同意Udi Dahan意见,不要将任何东西注射到实体中,这样的危险就是导致实体不是主体,而成为一个被动体,成为被动体的危险就是容易导致贫血模型。

而Domain Event模式可以让实体成为事件的发生源,成为主体。

我在Jdon框架6.1版本中引入了异步观察者模式,是这样考虑的:在众多实体模型关系中,分两大类:第一类是紧密关联,也就是以聚合关系存在的,这些对象们以DDD中聚合边界为范围紧密团结在一起,如JiveJdon的ForumThread和ForumMessage们,第二种:还有一些关系并不如聚合关联那么紧密,但是和核心模型有关联,是一种非常松散的关联,如何实现他们之间变动事件的传递?

这种事件消息传递有两种方式:同步和异步,Udi Dahan博客中提出的是Command性质的同步,而我提出引入异步观察者模式来实现Domain Event的异步,底层实现主要是借助Jdk 6.0的并发Conncurrent模型实现了异步事件处理功能,见Jdon框架源码:com.jdon.async.EventProcessor。

异步观察者模式步骤和JDK提供的同步观察者模式肥差类似:
1. 继承TaskObserver,实现其action方法,这是激活后所要实现的方法。
2. 将观察者TasjObserver加入ObservableAdapter。
3. 将设置观察点,在被观察或监听的类中,调用ObservableAdapter,在具体激活方法中调用ObservableAdapter的notifyObservers方法。

回到JiveJdon的主题订阅实现中,这样,我在ForumThread中引入一个对象ObservableAdapter被观察点,在ForumThread的仓储构造ThreadDirector,创建ForumThread时,为其注入new ObservableAdapter(com.jdon.async.EventProcessor.eventProcessor),这个EventProcessor相当于Udi Dahan
的Domain Event底层实现(可见其博客上源码)。

这样,在ForumThread中addNewMessage方法中增加subscriptionObservable.notifyObservers(args),这是激活观察者的一个事件或消息,由此观察者com.jdon.jivejdon.model.subscription.SubUpdateObserver的action方法就被异步激活,也就是说,这个action的执行是不妨碍主程序addNewMessage方法的执行,两者是两个线程同步并行实现的,这也是异步的好处,可以充分利用多CPU好处。action方法是查找数据库中该主题订阅的用户,这个过程
是可能缓慢的,因为和addNewMessage分开执行,因此,用户回复一个主题时调用addNewMessage,并不会因为缓慢的action方法而拖慢响应,用户回复主题的性能和速度是快速的,这里也体现良好DDD设计是高性能的一个保障。

这个使用异步观察者实现的Domain Event代码可以见最新的JiveJdon3.7源码。

感谢Udi Dahan的博文,否则我不会有将我自己实现Domain Event经历和构思写出来,因为发现我这种解决思路可以为更多人提供参考。


[该贴被admin于2009-10-12 12:02修改过]

谢谢banq
问一下,你是怎样找到这些文章的?呵呵

哈哈,这问题也困挠着我....终于有救世主了
[该贴被r7raul于2009-10-11 20:47修改过]

豁然开朗。 多谢

有一个号称是DDD的框架:bastion,其作者的博客有些内容比较有意思,注意加粗部分
A domain should be ever-expanding, it knows no boundaries. Changes in requirements should lead to additions, not changes to the domain.
A domain should always be live, always active.
A domain should be able to execute business processes dynamically. As business processes change continually, a dynamic domain model is a better way to support them than a process model, which is static.
A domain should have no infrastructural dependencies (e.g. persistence, authentication, logging). Instead, it is surrounded by adapters listening to events happening in the domain. Adapters handle these events by calling on external services. As these services are independent of the domain, they can be made generic and therefore reusable between domains.
A domain should be modelled using the time-inversion pattern and the active-passive pattern. That is: start modelling behaviour at the end of the process (in the spirit of demand chain management), and model passive objects in the real world as active ones in the domain model.


原文地址:http://www.blog.dannynet.net/archives/125
[该贴被oojdon于2009-10-12 09:34修改过]

>it is surrounded by adapters listening to events happening in the domain. Adapters handle these events by calling on external services.

非常棒,这里adapters listening实际就是Observer模式,监听者模式和观察者模式原理基本一样,可以理解为同一模式。

监听者模式和事件模式是紧密联系的。通过监听模式引入,可以将领域模型和服务以前其他底层的一些操作进行松耦合,从另外一个角度来说,注射IOC模式对于解决聚合性质的耦合比较擅长,对于非常活跃的事件模式,则GOF的行为模式中各个模式值得借鉴,其中包括Command模式和观察者模式。

由此也可以看出,用好DDD的基础是GoF设计模式。

bastion这个开源DDD框架虽然很简单,但是它对我的启发很大,特别是它将观察模式固化到Domain这个核心类中,用来辅助领域模型和外界的事件交互,通过Event和Message来实现模型和服务等外界交互,这个想法和我在Jdon框架中的异步观察者模式有异曲同工之妙:

bastion的Domain中事件触发方法:


protected <T extends DomainMessage> T notifyInternal(T message) {
List<Adapter> messageAdapters = adapters.get(message.getClass());
if (messageAdapters != null) {
for (Adapter<? super DomainMessage> adapter : messageAdapters) {
adapter.handle(message); //激活每个监听者的handle方法
}
}
return message;
}

JiveJdon中ForumThread的事件触发方法:

private void notifyObservers(Subscribed subscribed) {
if (subscriptionObservable != null) {
Object[] args = new Object[] { subscribed };
subscriptionObservable.notifyObservers(args); //激活每个观察者
}
}

两者区别之处是:bastion将之鲜明整入Domain这个核心类中,而Jdon框架则没有如此显式和Domain挂钩,看来Jdon框架可以跨出这一步,因为这个Domain Event是非常重要的普遍的一个DDD中解决方案。


[该贴被banq于2009-10-12 10:45修改过]
[该贴被admin于2009-10-12 12:02修改过]

注射IOC模式 不适合DomainBootstrap的生成?

何时注册那些监听器?ACTION生成的时候?

>何时注册那些监听器?ACTION生成的时候?
bastion中是在threadlocal中开始注册的,也就是一个请求开始时,就是当前这个实体对象被创建时,将监听器注册到其中,因为一般实体对象都一直活着,在缓存内存中,因此,这个实体对象以后还是可以继续加入新的监听者的。

我准备在Jdon框架中让监听器注册由框架自动完成,而不是现在由应用者自己完成,这样,会更方便。

我解决规则的时候就是这么注册的,但是注册进实体好像要复杂一些,感觉应该是在工厂和dao的查询返回前注册,也曾经弄过一两个demo,但都是手工注册的。

如果框架能解决那最好,现有SPRING能实现自动注册吗?多个实体能共有一个注册吗?

其实可以在Jdon框架或Spring框架中直接使用bastion框架,bastion框架主要是让除了领域模型以外的组件模型成为其卫星,就象太阳是核心,其他都绕着太阳转。

补充:DDD的事件顺序图如下:



[该贴被banq于2009-10-15 09:30修改过]


qq空间的留言回复通知应该也用的这个思路
[该贴被fxltsbl3855于2009-10-15 15:15修改过]

bastion框架集成SPRING,有例子吗?

>bastion框架集成SPRING
应该可以,Spring其实是和Domain Model无关的技术框架,以Domain Model观点看来,Domain Model就是与计算机概念无关的,而Spring属于那种和计算机有关的概念。

bastion框架是计算机概念和领域模型的结合部位,所以,两者能够使用。

我目前发现JavAte比Bastion更加全面,对DOmain Events处理也更加丰富,可见:
JavAte
[该贴被banq于2009-10-17 09:18修改过]