Domain Events – 救世主

原文 http://www.jdon.com/jivejdon/thread/37289/15

 

在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代码可以见最新的 JiveJdon 3.7源码。

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

有一个号称是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

 

>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 中解决方案。

 

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

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

 

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

 

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

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



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


 

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

 

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

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

我目前发现JavAte比Bastion更加全面,对DOmain Events处理也更加丰富,可见:
JavAte

 

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

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值