enode框架step by step之事件驱动架构(EDA)思想的在框架中如何体现

上一篇文章,我给大家分享了我的一个基于DDD以及EDA架构的框架enode,但是只是介绍了一个大概。接下来我准备用很多一篇篇详细但不冗长的文章介绍每个点。尽量争取一次不介绍太多内容,但希望每次介绍完后都能让大家知道这个小点的设计思想,以及为了解决的问题。

好了,这篇文章,我主要想介绍的是EDA思想在enode框架中如何体现?

经典DDD的基于领域服务的实现方式

一般的应用程序,如果一个用户动作会涉及多个聚合根的修改,我们通常会在应用层服务中创建一个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的事件驱动的实现方式

我一直说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?

总结:

我之所以比较喜欢事件驱动这种思想是基于以下理由:

  1. 就是上面我说的解耦+可扩展;
  2. 事件可以并行执行;就是说,一个系统中,同时可以有很多事件在并行的产生、传递、响应;这样说,大家可能还理解不了这一点的价值。我说一下并发的概念。通常我们所说的一个网站的并发,比如有5000,是指一个网站在1秒内的所有并发请求数,这么多并发请求数是针对系统中所有的聚合根的;也就是如果平摊到每个聚合根,那并发修改数一般就很低了,比如每秒只有10个并发,甚至只有1个或两个。这点每个系统有所不同,比如淘宝的商品秒杀活动,那当秒杀开始的时刻,对同一个商品的下单的并发数很高,因为每个商品的每个订单都意味着要减库存,所以这个减库存的并发操作一定很高,实现起来肯定很困难了,不通过可靠的分布式缓存以及乐观锁机制,估计很难实现;而比如新浪微博上,我们每个人发微博,虽然整个新浪微博网站的整体并发数很高,因为肯定每秒有非常多的人在写微博,但是我们同时也知道,大家写的微博都是独立的,没有共享资源,每发表一条微博实际上就是创建一条数据库记录而已。所以可以理解为,单个对象无并发;而一般的企业应用或一般的互联网应用,针对同一资源(同一个聚合根)的并发修改,一般都不高;所以基于这样的分析和理解,我们知道了,理论上,事件什么时候可以并行产生和执行,什么时候必须排队。就是:如果两个事件不是同一个聚合根产生的,那就可以并行处理,事件也可以并行持久化;如果是单个聚合根产生的,那必须按照顺序被持久化;所以,根据这样的理解,我们知道了,一个应用程序,除了单个聚合根上的修改只能串行进行外,其他情况理论上都可以并行执行;这段话说了这么多关于并发数以及事件并行方面的东西,那究竟知道这些有什么用呢?很简单,只要和传统的事务模式对比下就知道了,传统的事务模式,如果要修改多个聚合根,那事务在执行的那一段时间,所有涉及到的聚合根都不能被其他事务所修改;只有等到当前事务执行完成后,其他事务才能执行;而通过事件的方式,由于我们没有事务的概念,我们唯一要确保的只是一个聚合根上产生的事件必须被一个个按顺序持久化,这点我们很简单,比如我们只要建一个联合主键:聚合根ID+事件版本号,然后做乐观并发控制即可;所以,事件持久化时,排他的粒度比事务要小,这样的好处是无阻塞;那么换来的好处就是网站整体的可用性高;但是带来的坏处是,可能有可能会出现乐观并发冲突,但这点我们可以通过框架的自动重试功能解决掉;而且,我们也刚分析过,同一个聚合根的并发修改一般是很低的;所以通过事件的方式来达到这种细粒度的对聚合根的修改是非常有意义的。
  3. 配合Event Sourcing模式,可以让EDA发挥更大的价值,更准确的说,我们可以让事件发挥更大的价值;就是:我们不仅可以让事件作为消息,在系统各个对象或组件甚至是各个系统之间传递,还可以用事件来还原整个系统的状态。这点我会在后面详细介绍enode框架中如何使用event sourcing这种模式;
 
 

enode框架step by step之事件驱动架构(EDA)思想的在框架中如何体现的更多相关文章

  1. 后端开发实践系列之三——事件驱动架构(EDA)编码实践

    在本系列的前两篇文章中,笔者分别讲到了后端项目的代码模板和DDD编码实践,在本文中,我将继续以编码实践的方式分享如何落地事件驱动架构. 单纯地讲事件驱动架构(Event Driven Architec ...

  2. 基于事件驱动的DDD领域驱动设计框架分享(附源代码)

    原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...

  3. IDDD 实现领域驱动设计-CQRS(命令查询职责分离)和 EDA(事件驱动架构)

    上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动架构 Domin Event-领域事件 Long-Runni ...

  4. CQRS(命令查询职责分离)和 EDA(事件驱动架构)

    转载CQRS(命令查询职责分离)和 EDA(事件驱动架构) 上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动 ...

  5. enode框架step by step之消息队列的设计思路

    enode框架step by step之消息队列的设计思路 enode框架系列step by step文章系列索引: enode框架step by step之开篇 enode框架step by ste ...

  6. enode框架step by step之框架的物理部署思路

    enode框架step by step之框架的物理部署思路   enode框架系列step by step文章系列索引: enode框架step by step之开篇 enode框架step by s ...

  7. enode框架step by step之saga的思想与实现

    enode框架step by step之saga的思想与实现 enode框架系列step by step文章系列索引: 分享一个基于DDD以及事件驱动架构(EDA)的应用开发框架enode enode ...

  8. enode框架step by step之Staged event-driven architecture思想的运用

    enode框架step by step之Staged event-driven architecture思想的运用 enode框架系列step by step文章系列索引: 分享一个基于DDD以及事件 ...

  9. enode框架step by step之框架要实现的目标的分析思路剖析1

    enode框架step by step之框架要实现的目标的分析思路剖析1 enode框架系列step by step文章系列索引: 分享一个基于DDD以及事件驱动架构(EDA)的应用开发框架enode ...

随机推荐

  1. select查询原理

    原文:select查询原理 我并非专业DBA,但做为B/S架构的开发人员,总是离不开数据库,一般开发员只会应用SQL的四条经典语句:select ,insert,delete,update.但是我从来 ...

  2. 基于Cocos2dx + box2d 愤怒的小鸟的实现Demo

    1. Demo初始界面 2. 游戏界面 3. 精确碰撞检測 4. 下载  压缩文件文件夹 AngryBird source    愤慨的小鸟Demo源码,基于Cocos2dx C++,以及box2d技 ...

  3. C# WebBrowser 代理的使用

    原文:C# WebBrowser 代理的使用 The WebBrowser control is just an embeddded IE Control, I believe any setting ...

  4. 移动客户端与服务端Session那点秘密

    众所周知,做过Web开发的小伙伴可能知道,在浏览器向服务器发一个请求,服务器端会为当前的访问者创建一个session会话,随着浏览器的关闭而会话结束.但是移动客户端咋整呢(IOS/Android啥的) ...

  5. Beginning Python From Novice to Professional (5) - 条件与循环

    条件与循环 条件运行: name = raw_input('What is your name? ') if name.endswith('Gumby'): print 'Hello, Mr.Gumb ...

  6. 第3章1节《MonkeyRunner源码剖析》脚本编写示例: MonkeyRunner API使用示例(原创)

    天地会珠海分舵注:本来这一系列是准备出一本书的,详情请见早前博文“寻求合作伙伴编写<深入理解 MonkeyRunner>书籍“.但因为诸多原因,没有如愿.所以这里把草稿分享出来,所以错误在 ...

  7. Fedora21无法播放MP4[已解决]

    首先,安装安装rpmfusion源 http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-21.noarch.rpm ht ...

  8. SQL点滴10—使用with语句来写一个稍微复杂sql语句,附加和子查询的性能对比

    原文:SQL点滴10-使用with语句来写一个稍微复杂sql语句,附加和子查询的性能对比 今天偶尔看到sql中也有with关键字,好歹也写了几年的sql语句,居然第一次接触,无知啊.看了一位博主的文章 ...

  9. leetcode 第43题 Wildcard Matching

    题目:(这题好难.题目意思类似于第十题,只是这里的*就是可以匹配任意长度串,也就是第十题的‘.*’)'?' Matches any single character. '*' Matches any ...

  10. nginx+lua+redis高并发应用建设

    ngx_lua将lua嵌nginx,让nginx运行lua脚本.高并发,非堵塞过程中的各种请求. url要求nginxserver,然后lua查询redis,返回json数据. 一.安装lua-ngi ...