锯缘圆龟图片:Domain Events – 救世主

来源:百度文库 编辑:九乡新闻网 时间:2024/04/29 00:04:26

在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 repository,
LoggingService service);

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

IList 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经历和构思写出来,因为发现我这种解决思路可以为更多人提供参考。


-----------------------------------------------------------------------------------------------

>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 extends DomainMessage> T notifyInternal(T message) {
List messageAdapters = adapters.get(message.getClass());
if (messageAdapters != null) {
for (Adaptersuper 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修改过]


-------------------------------------------------------------------------------r7raul

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


banq 

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

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


freebox 

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


banq 

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

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



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


fxltsbl3855 

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


banq 

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

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

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


winter 

看了几篇Udi的文章,通过Domain event确实可以避免Repository和Service的介入,使Entiry专注于业务逻辑,不过对于不同的业务需求我们就需要增加大量的handler其实现就是repository和service的相关代码,感觉有些不爽


Abramdy 

bastion框架中的适配监听器的触发机存在这样的问题:
领域消息事件与适配监听器是一对多的关系,按照现在的设计,同一个消息事件可能会触发多个适配监听器,不甚合理。例如,A、B模块需要在同一个查询消息事件下绑定各自的查询适配监听器,A模块的发送的查询消息事件同时会触发B模块的查询服务。是否需要为在同一消息事件下绑定的不同适配监听器设置标示符,从而能够和领域消息事件中存储标示符匹配?
[该贴被Abramdy于2009-12-02 14:47修改过]


banq 

2009年12月02日 14:43 "Abramdy"的言论bastion框架中的适配监听器的触发机存在这样的问题:
同一个消息事件可能会触发多个适配监听器,不甚合理。
[该贴被Abramdy于2009-12-02 14:47修改过]

是的,我从Jdonframework 6.2开发中也感觉这个问题,原来设置观察者模式是一对多,结果发现使用变得复杂,现在改为一对一,简单。