ENode框架Conference案例分析系列之 - Quick Start
前言
前一篇文章介绍了Conference案例的架构设计,本篇文章开始介绍Conference案例的代码实现。由于代码比较多,一开始就全部介绍所有细节,估计很多人接受不了,也理解不了。所以,我先进行一次QuickStart的介绍,即选取某个简单典型的场景从前到后过一下每个环节。这样大家就能够快速对代码的重要关键环节有大概的理解。另外,我现在正在做ENode的官网,到时会像axon framework一样,介绍ENode框架本身、使用场景、性能数据、案例,以及论坛社区等功能;
本文打算选择Conference案例中一个不太复杂的场景(发布会议),来快速过一下需要开发者实现的代码环节。
UI
首先,我们来看一下发布一个会议的UI入口,前面的文章介绍过,当客户创建好一个会议后,他可以先编辑会议的所有座位类型,然后如果允许预订者预定了,那他就需要先发布这个会议。就像淘宝的卖家要上架商品后商品才会对买家可见一样。本质上,发布一个会议,其实就是将会议聚合根的isPublished修改为true。UI如下图所示:
Controller
当客户点击Publish按钮后,前台提交HttpPost请求到服务端,然后请求就会被ASP.NET MVC的Controller处理,Controller的Action的逻辑如下:
上面的代码比较清晰,我们先判断当前的_conference实例是否为空,如果为空,则直接返回HttpNotFound结果。那_conference实例哪里来的?这里考虑到ConferenceController中的大部分Action都要使用当前的_conference实例,所以我们为了代码的复用,在Controller的OnActionExecuting方法里,提前获取了当前的_conference实例,代码我稍后再贴。_conference实例有了之后我们就可以构建一个PublishConference的命令。该命令只需要一个当前要发布的Conference的ID即可。然后,我们调用ExecuteCommandAsync方法,去异步执行一个命令。然后,我们使用await关键字异步等待命令的处理结果。最后判断结果是否成功,做相应的处理。
ExecuteCommandAsync方法:
该方法内部使用_commandService的ExecuteAsync方法来异步执行一个命令。_commandService是ENode框架提供的分布式的命令发送或执行服务,该服务是通过ConferenceController的构造函数注入的,代码如下:
使用ENode框架开发的Controller,一般是需要两个服务,一个是commanService,用于发送命令;另一个是某个queryService,用语查询数据;Controller依赖这两个服务充分体现了CQRS架构的特点。当然,有时查询服务可能不止一个,那就可以注入多个,看我们自己需要即可。ENode使用的依赖注入框架是Autofac。大家可能在想,为何要弄一个ExecuteCommandAsync方法出来呢?因为要处理超时的情况,假如一个命令处理超时了(比如5s),那Controller的Action也需要立即返回了。TimeoutAfter的代码如下:
大家可以看到TimeoutAfter方法内部,为了实现当超过指定时间后要求的Task还未处理完的情况,我们创建了一个延后指定时间执行的Task,然后通过Task.WhenAny方法异步等待任务执行,最后判断完成的Task是哪个,从而实现超时的处理。这个做法是我在网上找到的,觉得还不错,这个做法可以让我们在实现完全异步的同时还能实现超时处理。最后,我们看一下OnActionExecuting方法:
OnActionExecuting
这个方法的代码的逻辑也比较简单,就是根据HttpRequest中包含的slug参数先获取一个Conference聚合根;如果存在,则进一步根据accessCode参数检查accessCode参数是否合法。通过合法,则认为提供的slug和accessCode有效。大家可以把slug理解为唯一定位一个Conference的,而accessCode是使用该Conference的密码。由于这个只是一个案例,所以我们通过这种简单有效的方法来为用户授权。
Command
了解了Controller的实现,我们接下来看看Command的定义,Command是一个DTO对象。代码如下:
非常简单,由于ENode框架基类提供的Command类已经提供了一个AggregateRootId的属性,所以我们的PublishConference命令无需再定义其他额外的属性了。需要提一下的是,ENode框架要求,所有Command要创建或修改的聚合根的ID必须在Command发送之前赋值,这个是框架的一个约束,我认为这个通常不是问题。如果你希望聚合根的ID是一个long,那也许你需要自己部署一个全局long生成服务了,有兴趣的朋友可以和我交流实现方案,我有实现经验。如果你的ID是一个字符串,那用ENode框架提供的ObjectId类即可,它可以帮你自动生成一个24位长度的全局唯一顺序字符串。接下来我们看看Command Handler的实现。
Command Handler
一个CommandHandler中的代码通常是一句话,ENode框架的最大好处是可以让开发者无需关注C端的技术实现,开发者只需要关心如何实现自己的业务逻辑即可。如上图所示,我们会先定义一个ConferenceCommandHandler的类,然后实现ICommandHandler<PublishConference>接口,然后进一步实现对应的Handle方法。在Handle方法内部,我们只需要从当前的上下文根据Command所关联的聚合根ID获取当前要操作的聚合根,然后调用聚合根的业务方法即可。我们不需要像经典DDD那样把聚合根从IConferenceRepository中取出来,再修改聚合根,再保存聚合根。并且经典DDD往往还会和工作单元(Unit of Work)配合;因为经典DDD,是支持一个应用层的方法同时修改多个聚合根的,而ENode框架是要求一个命令一次只能创建或修改一个聚合根,即架构设计上就是面向最终一致性的,主要目的为了实现更高的吞吐量,这点开发者需要明确与了解。CommandHandler,从代码实现的角度,我相信ENode框架提供的方式是非常简单和直接的,没有任何多余的东西。大家可以看到使用ENode框架开发,大部分情况是不需要定义Repository的。下面我们来看看Domain聚合根的实现。
Domain & Domain Event
使用ENode框架开发的领域模型,聚合根的实现通常是这样的:
- 继承基类AggregateRoot<TAggregateRootId>
- 构造函数要传给基类聚合根的ID
- 然后聚合根自己会提供一些可以修改自己状态的公共方法,例如上面的Publish,Unpublish方法
- 聚合根内部会有一些私有的Handle方法,这些Handle方法是根据对应的事件,更新自己的状态(事件驱动聚合根状态的修改)
当前我们这里被调用的方法是Publish,该方法内部,先判断当前聚合根是否已经处于发布状态,如果是,则抛出异常即可,当然,你选择忽略也没问题;如果不是,则调用ApplyEvent方法Apply当前领域事件。ApplyEvent方法的逻辑是,先找到当前事件对应的Handle方法,然后调用该Handle方法;然后调用完成后,把当前事件放入一个聚合根内部的事件队列中。
如果对ENode框架的实现有一定了解的朋友应该知道,ENode在处理一个命令时,ENode框架处理Command的核心流程是这样的:
- 先创建一个空的ICommandContext对象;
- 调用CommandHandler的Handle方法,并把ICommandContext传给Handle方法;
- 当Handle方法结束后,ENode框架就能知道当前ICommandContext中有哪些聚合根修改了或创建了(框架要求一个命令一次只能涉及一个聚合根的修改)然后框架如果拿到了某个修改的聚合根,它就拿出该聚合根里上面提到的内部的事件队列里的事件。然后根据这些事件生成一个EventStream的对象;
- 持久化EventStream对象到EventStore;
- 发布(Publish)EventStream到EQueue,然后外部的Event Handler就都能响应领域事件了;
上面这个是正常流程,在这里我顺便提一下,为了让大家更好的理解内部实现的机制。通过上面这些介绍,我想大家应该至少可以理解上面的Publish方法和Handle方法了吧。
另外,有些朋友可能会想,为何是先产生事件,再修改状态呢?
主要原因是因为这个Handle方法是会在事件溯源(ES)的时候被重复利用的。当我们要从EventStore通过ES还原某个聚合根时,我们是先从EventStore获取该聚合根所产生的所有的事件,然后对每个事件调用聚合根的对应的Handle方法,从而实现聚合根状态的还原。这个过程也就是我们常说的事件溯源,即ES(Event Sourcing)。
需要强调的是,聚合根应该在产生事件之前把各种业务规则和业务逻辑实现掉,然后只有当前操作满足所有的业务规则时,才调用ApplyEvent方法。然后在聚合根里的所有Handle方法中,就是仅仅简单的等于号赋值操作,不能有任何业务逻辑,这点非常重要。为什么要这样呢?因为假如我们把一些业务规则和逻辑放在Handle方法中,比如if怎么样的时候做什么赋值,else的时候做另外的赋值。那假如哪一天我们的Handle方法里的判断逻辑变化了,那我们通过事件溯源还原出来的聚合根的状态就不对了。这点应该不难理解吧。
从更高层面(哲学)的角度来理解,EventStore中存储的事件并不是完整的历史。事件+聚合根的Handle方法才是完整的历史,两者结合才可以完整地将聚合根的状态还原到最新状态。因为是历史,历史无法改变,所以我们的事件和Handle方法也都不能修改;或者如果真的要修改,也必须确保兼容老的结构和实现,这点非常重要。下面我们来看看Event Handler的实现:
Event Handler
EventHandler的作用是根据C端聚合根产生的事件来更新CQRS的读库。需要注意的是ENode整个框架对外提供的API基本都是异步IO的(实际上内部的实现也都是异步IO的,只有整个链路都是异步得,才能发挥异步的好处)。所以我们更新读库时,需要使用ADO.NET提供的Async方法类更新DB。这里我使用ENode自带的Dapper轻量级高性能ORM来实现对读库的更新。上面的代码中就是更新Conference表的IsPublish字段。但是为了确保避免并发导致的数据覆盖,所以我们需要严谨的利用乐观控制来确保数据不会被覆盖,ENode要求我们使用Version机制来实现乐观锁。
关于并发控制的讨论,其实还有非常多的细节可以讨论。我之前写过一篇文章,大家有兴趣的可以去看一下,本文的目的是做一个QuickStart,所以不做过多展开了。TryUpdateRecordAsync方法的内部实现如下,很简单,我就不做介绍了。
还有一点需要特别提一下,就是为何要使用Dapper而不使用EF这种ORM框架。因为ENode框架实现的是CQRS+ES的架构。所以,我们在更新读库时,是根据事件更新读库。那怎么样的更新是最快的呢?就是直接通过Insert或Update语句来更新DB。而如果通过EF这种框架,因为是面向OO的ORM,所以一般是需要先从DB取出数据转换为对象,再更新对象,再保存对象这样的思路。这个过程我个人认为,对于CQRS+ES架构的应用来说,是比较繁琐和低效(2次IO,先读出来,再保存回去)的。我们在更新读库时,更好的方式应该是利用像Dapper这样的ORM框架,简单直接的更新读库(一次IO操作即可)。另外,我通过对Dapper做了一些简单的二次封装,可以做到用最直接的代码实现目的,且兼顾了代码的可读性、可维护性、灵活性,以及性能。另外,查询数据时,通过Dapper也非常简单,而且还支持返回dynamic对象。Dapper是基于约定的框架,不需要做ORM映射方面的配置。我个人认为使用在CQRS+ES架构中是非常合理的。所以,对我来说,EF可以退休了,呵呵。
总结
好了,上面介绍了发布会议的所有需要用户写的代码,是不是很简单呢?我个人认为和经典DDD的架构相比,由于有ENode框架的支持,所以开发基于CQRS+ES架构的应用,是非常简单的。下一篇要写什么还没想好,大家还想了解什么,可以及时给我反馈啊。
ENode框架Conference案例分析系列之 - Quick Start的更多相关文章
- ENode框架Conference案例分析系列之 - 文章索引
ENode框架Conference案例分析系列之 - 业务简介 ENode框架Conference案例分析系列之 - 上下文划分和领域建模 ENode框架Conference案例分析系列之 - 架构设 ...
- ENode框架Conference案例分析系列之 - 架构设计
Conference架构概述 先贴一下Conference案例的在线地址,UI因为完全拿了微软的实现,所以都是英文的,以后我有空再改为中文的. Conference后台会议管理:http://www. ...
- ENode框架Conference案例分析系列之 - 复杂情况的读库更新设计
问题背景 Conference案例,是一个关于在线创建会议(类似QCon这种全球开发者大会).在线管理会议位置信息.在线预订某个会议的位置的,这样一个系统.具体可以看微软的这个项目的主页:http:/ ...
- ENode框架Conference案例分析系列之 - 订单处理减库存的设计
前言 前面的文章,我介绍了Conference案例的业务.上下文划分.领域模型.架构,以及代码整体流程.接下来想针对案例中一些重要的场景,分别做进一步的分析.本文想先介绍一下Conference案例的 ...
- ENode框架Conference案例分析系列之 - ENode框架初始化
前言 Conference案例是使用ENode框架来开发的.之前我没有介绍过ENode框架是如何启动的,以及启动时要注意的一些点,估计很多人对ENode框架的初始化这一块感觉很复杂,一头雾水.所以,本 ...
- ENode框架Conference案例分析系列之 - 事件溯源如何处理重构问题
前言 本文可能对大多数不太了解ENode的朋友来说,理解起来比较费劲,这篇文章主要讲思路,而不是一上来就讲结果.我写文章,总是希望能把自己的思考过程尽量能表达出来,能让大家知道每一个设计背后的思考的东 ...
- ENode框架Conference案例分析系列之 - 上下文划分和领域建模
前面一片文章,我介绍了Conference案例的核心业务,为了方便后面的分析,我这里再列一下: 业务描述 Conference是这样一个系统,它提供了一个在线创建会议以及预订会议座位的平台.这个系统的 ...
- ENode框架Conference案例分析系列之 - 业务简介
前言 ENode是一个应用开发框架.通过ENode,我们可以方便的开发基于DDD+CQRS+EventSourcing+EDA架构的应用程序.之前我已经写了很多关于ENode的架构以及设计原理的文章, ...
- ENode框架Conference案例转载
ENode框架Conference案例分析系列之 - Quick Start 前言 前一篇文章介绍了Conference案例的架构设计,本篇文章开始介绍Conference案例的代码实现.由于代码比较 ...
随机推荐
- string与int互换
1:将string转化为int 1.) int i = Integer.parseInt(String s); 2.) int i = Integer.valueOf(my_str).intValue ...
- Centos7中安装Mysql及配置
CentOS 7 安装 MySQL 首先检查 MySQL 是否已安装 yum list installed | grep mysql 如果有的话 就全部卸载 yum -y remove +数据库名称 ...
- linux python更新
linux的yum依赖自带的Python,为了防止错误,此处更新其实是再安装一个Python 1.查看默认python版本 python -v 2.安装gcc,用于编辑Python源码 yum ins ...
- python 基础之数据类型
一.python中的数据类型之列表 1.列表 列表是我们最以后最常用的数据类型之一,通过列表可以对数据实现最方便的存储.修改等操作 二.列表常用操作 >切片>追加>插入>修改& ...
- [转] nodemon 基本配置与使用
在开发环境下,往往需要一个工具来自动重启项目工程,之前接触过 python 的 supervisor,现在写 node 的时候发现 supervisior 在很多地方都有他的身影,node 也有一个 ...
- js cookie的封装和调用
<script> function setCookie(cname,cvalue,exdays){ var d = new Date(); d.setTime(d.getTime()+(e ...
- [VijosP1639]机密文件 题解
题目大意: m个人抄n份资料,资料有编号,每人抄连续的几份资料,每份资料页数不一定相等,每个人抄的速度相同,求使得总时间最少的方案(总时间相同,越前面的人抄的越少) 思路: 假设每人一天抄一页,二分天 ...
- HDU 4801 Pocket Cube
题目链接 去年现场,虎哥1Y的,现在刷刷题,找找状态... 一共6种转法,把3个面放到顶部,左旋和右旋,感觉写的还不错....都写成常数了. #include <stdio.h> #inc ...
- 服务器使用FTP命令行 无法传送文件 卡在150 Opening data channel for file transfer
猜测,是因FTP服务器采用了主动模式,在创建数据传输通道时,服务器会以一个随机的端口,连接回来. 临时解决方案: 因不知道请求回来使用的哪个段的端口,因此,暂时关闭了防火墙.即能正常传输文件了.
- c# 局域网文件传输实例
一个基于c#的点对点局域网文件传输小案例,运行效果截图 //界面窗体 using System;using System.Collections.Generic;using System.Compon ...