开源地址:https://github.com/tangxuehua/enode
上一篇文章,我给大家分享了我的一个基于DDD以及EDA架构的框架enode,但是只是介绍了一个大概。接下来我准备用很多一篇篇详细但不冗长的文章介绍每个点。尽量争取一次不介绍太多内容,但希望每次介绍完后都能让大家知道这个小点的设计思想,以及为了解决的问题。
好了,这篇文章,我主要想介绍的是EDA思想在enode框架中如何体现?
一般的应用程序,如果一个用户动作会涉及多个聚合根的修改,我们通常会在应用层服务中创建一个unit of work,然后,我们可能会设计一个领域服务类,在该领域服务类里,修改多个聚合根,然后应用层服务将整个unit of work中的修改一次性以事务的方式提交到数据库。这种方式就是以事务的方式来实现涉及多个聚合根修改的强一致性。以银行转账这个经典的场景作为分析案例:
public interface IBankAccountService { void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount); } public class BankAccountService : IBankAccountService { private IContextManager _contextManager; private TransferMoneyService _transferMoneyService; public BankAccountService(IContextManager contextManager, TransferMoneyService transferMoneyService) { _contextManager = contextManager; _transferMoneyService = transferMoneyService; } public void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount) { using (var context = _contextManager.GetContext()) { var sourceAccount = context.Load<BankAccount>(sourceBankAccountId); var targetAccount = context.Load<BankAccount>(targetBankAccountId); _transferMoneyService.TransferMoney(sourceAccount, targetAccount, amount); context.SaveChanges(); } } }
一次银行转账,最核心的动作就是源账号转出钱,目标账号转入钱;当然实际的银行转账肯定不是这么简单,也肯定不是这么实现。我拿这个作为例子只是为了通过这个大家都熟知的简单例子来分析如果一个用户场景涉及不止一个聚合根的修改的时候,如果基于经典的DDD的方式,我们是如何实现的。如上面的代码所示,我们可能会设计一个应用层服务,如上面的IBankAccountService,该应用层服务里有一个TransferMoney的方法,表示用于实现银行转账的功能;然后该应用层服务会进一步调用一个领域层的转账领域服务,就是上面代码中的TransferMoneyService,按照Eric Evans所说,领域服务应该是一个以动词命名的服务,一个领域服务可以明确对应到领域中的一个有业务含义的领域动作,此例就是“转账”,所以我设计了一个TransferMoneyService的以动词来命名的领域服务,该服务的TransferMoney方法实现了银行转账的核心业务逻辑。
上面这个例子中,按照经典DDD,我们应该在应用层实现流程控制逻辑以及事务等东西;所以大家可以看到,以上代码中,我们是先获取一个unit of work,即上面代码中的context,最后调用context.SaveChanges方法,该方法的职责就是将当前上下文的所有修改以事务的方式提交到数据库。好了,上面这个例子我们分析了经典DDD关于如何实现一个会涉及多个聚合根新建或修改的用户场景;
我一直说enode是一个基于事件驱动架构(EDA,Event-Driven Architecture)的框架。且深蓝医生在前面的回复中也对什么是事件驱动的架构有疑惑。所以我想说一下我对事件驱动架构的理解。
EDA,顾名思义,我觉得就是事件驱动的,那事件到底驱动了什么呢?我觉得就是事件驱动状态的修改。如何理解呢?就是说,假如你要修改一个对象的状态,那就不是直接调用该对象的某个方法来修改它或者直接通过修改某个对象的属性来达到修改该对象状态的目的;取而代之的是,我们需要先触发一个事件,然后该对象会响应该事件,然后在响应函数中修改对象自己的状态。当然,更广义和权威的事件驱动架构的定义和解释,我觉得很容易找啊,比如直接去百度上搜一下或直接到wikipedia上搜一下,也很容易就能找到标准的解释。比如这里就是我找到的解释。其实,更大范围的解释,就是一种publish-subscriber模式,就是有一个事件生产者产生事件,然后有一个类似event publisher的东西会把这个事件广播出去,然后所有的事件消费者就能消费该事件了。通过这样的pub-sub,我们的应用程序的各个组件之间可以做到很彻底的解耦,并且可以做到更灵活的扩展性。这两点的好处应该是很容易体会到的。比如更彻底的解耦是,比如本来一个对象要和另一个对象交互,那它可能要引用该对象,然后调用该对象的某个方法,从而实现对象之间的交互。这种实现方式会让两个对象绑定在一起,比如a对象调用b对象的方法,那意味着a需要依赖b对象;而通过事件驱动的方式,a对象只要publish一个事件,然后b对象响应该事件即可,这样a对象就不知道b对象的存在了,也就是a对象不在依赖b对象;扩展性,就是本来一个事件,可能只有1个事件响应者,但是后面可能由于功能扩展等原因,我们需要增加一个事件响应者,这样就能方便的做到在不改变原来任何代码的基础之上,增加新功能了;其他的好处就不多分析了,有兴趣的可以再去看看资料吧。
上面这一段,我简单介绍了我所理解的EDA,以及它的基本的好处。下面我们看看,在enode中,我们是如何利用EDA这种原理的。为了简化,我先用一个简单的例子说明一下,就用我源代码中的NoteSample吧,反正也能一样说明事件驱动的影子在哪里。看以下的代码:
[Serializable] public class Note : AggregateRoot<Guid>, IEventHandler<NoteCreated>, //订阅事件 IEventHandler<NoteTitleChanged> { public string Title { get; private set; } public DateTime CreatedTime { get; private set; } public DateTime UpdatedTime { get; private set; } public Note() : base() { } public Note(Guid id, string title) : base(id) { var currentTime = DateTime.Now; //触发事件 RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime)); } public void ChangeTitle(string title) { //触发事件 RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now)); } //事件响应函数 void IEventHandler<NoteCreated>.Handle(NoteCreated evnt) { //在响应函数中修改自己的状态,这里可以体现出EDA的影子,就是事件驱动状态的修改 Title = evnt.Title; CreatedTime = evnt.CreatedTime; UpdatedTime = evnt.UpdatedTime; } //事件响应函数 void IEventHandler<NoteTitleChanged>.Handle(NoteTitleChanged evnt) { //同上解释 Title = evnt.Title; UpdatedTime = evnt.UpdatedTime; } }
上面的例子中,Note是一个聚合根,它会响应两个事件:NoteCreated, NoteTitleChanged。要实现事件响应,我们可以通过实现框架提供的IEventHandler<T>接口,就能告诉框架,我要订阅什么事件了。
上面代码中,应该比较详细的注释了每段代码的含义了,应该都能看懂吧。上面这个例子说明了,聚合跟自己的状态不是在public方法中直接改的,而是基于事件驱动的方式来修改的,所以,大家可以看到,聚合根状态的修改是在一个内部响应函数中修改的。下面我们再来看一下外部其他对象,如何响应该事件:
//这是一个事件订阅者,它也响应了Note的两个事件 public class NoteEventHandler : IEventHandler<NoteCreated>, IEventHandler<NoteTitleChanged> { public void Handle(NoteCreated evnt) { //这里为了简单,所以只是输出了一串文字,实际我们可以在这里做任何你想做的事情; Console.WriteLine(string.Format("Note created, title:{0}", evnt.Title)); } public void Handle(NoteTitleChanged evnt) { Console.WriteLine(string.Format("Note title changed, title:{0}", evnt.Title)); } }
通过上面两个简单的例子,不知道有没有解释清楚,在enode框架中,如何体现EDA?
总结:
我之所以比较喜欢事件驱动这种思想是基于以下理由: