这篇文章应该算是对前三篇的一个补充,在写之前说个题外话,有园友评论这是在用三层架构在写DDD,我的个人理解DDD是一种设计思想,跟具体用什么架构应该没有什么关系,DDD也需要分层,也有三层架构的影子在里面。三层架构主要是表现层、业务层和数据层,而DDD已经没有数据层,三层结构里的模型是贫血的,而DDD却是充血的。如果你在用三层框架已经有了聚合,实体,值对象的概念,那说明你已经在靠近DDD了,或者你不愿相信罢了,当然你可以保留自己的观点,这里不作争论,我也不能作出结论,我个人是觉得这种讨论也是有意义的,我也会思考之前所介绍的到底是不是DDD,这个答案留给各位读者吧。总之欢迎形式各样的评论。
接下来我就来介绍一下CQRS(命令和查询职责分离 )风格的框架。在学习CQRS的时候,有很多人说这个太高大上,难以应用。我想说CQRS不是那么可怕,当然也不是那么简单。那么就开始慢慢来揭开面纱。首先还是先看看经典DDD在Application层是这么做的,先定义一个接口
public interface IUserService
{
UserDTO GetUserInfo(string userId);
IEnumerable<UserDTO> GetAllUsers(); void RegisterUser(UserDTO userData);
void ModifyContactInfo(UserDTO userData);
void ModifyPassword(string userId, string oldPwd, string newPwd);
}
通过代码会发现定义的接口有一点点规律,要么是有返回值,要么就是没有返回值的,那么他们有什么特点呢?请注意我在写代码的时候特意在两个接口之间加了回车以区分,上面两个主要为了返回数据,是查询,下面三个其中一个是创建数据,剩下的是修改数据,是命令。也就是说一个方法要么是执行某种动作的命令,要么是返回数据的查询且查询不应该会影响数据,不可能两者同时存在(可能你并不认同,有一种情况是特殊的,就是当实体标识由数据库来提供的,那么有时我们需要知道它的标识,但我也建议该方法不应该有返回值,可以用out,或者是给传输对象进行赋值,在有些环境下后一种也并不能解决),也就类似一次向服务器发送url请求时,要么是get,要么是post,不可能即是get,又是post。
当前接口只定义了一个DTO,该DTO的描述可能会过于宏大,只有当我们知道需要调用哪个接口时才会知道此DTO有哪些数据,于是当ui层去对DTO赋值往往也会不知所措,你是不是有针对不同的接口去定义相应的DTO的想法呢?至少我有,那么这样的DTO和接口是不是具有相同功能的表达呢?我已经开始会将上页分成两个接口了
再来说说查询,查询主要是为了ui呈现数据,经典DDD的查询一般都是通过repository(具体实现很多情况下会选择orm),然后将domain model转成dto,这种方式限制很大,对于一些复杂数据就会显得很难,如当要查看一个user信息时还要展示他的role信息,这样就需要通过repository先查出user,然后再通过user.RoleID再查询role,最终数据转换成ui需要的model,应用层开发就会有点繁琐了,不如关系数据库一句sql来的方便。当然,现在的orm(如nh、ef)提供了级联查询,这样就会在user上定义一个role属性,虽然是方便了很多,但是这样的回报也仅仅是为了查询,对于我们跟踪状态一点用也没有,为什么?当一个用户修改角色时,需要role对象吗?不需要,只需要他的ID,因此在聚合之间的引用应该尽量用引用ID,而不是引用对象,所以聚合之间尽量低耦合,“低耦合高内聚”这个标准也能够更好进行模块式开发。再有一些汇总查询,估计repository实现人员快要疯了,写应用层的人估计更要疯,呵呵。使用orm带来的好处是显而易见的,但是面对查询,orm并不是那么完美,尽管现在的orm查询功能已经很强大。经过以上阐述你可能有了一点想法,让查询绕开仓储。
接下来就开始CQRS吧。不细说查询了,在上述接口重新定义一个名称叫做IUserQuerySerice,我已经开始注重命名了,去掉里面的命令方法就行了。那么只要针对ui展示数据用的查询DTO就行了,他也可以叫ReadModel(只读模型),我个人觉得这个叫法更合适一点。那么实现你用数据库视图也行,用sql也好,达到目的就行。还要就是需要定义多少ReadModel,这个仁者见仁,智者见智。
重点是命令处理,为C端设计一个接口
public interface ICommandBus
{
void Send(ICommand command);
}
就这么简单,但是这带来了需要大量写Command,即每有一个操作就需要定义一个命令模型,然后还要写该命令对应的处理器,还是拿之前的用户注册的例子来演示代码吧
public class RegisterUserCommand : ICommand
{
public string Name { get; set; }
public string Password { get; set; }
public string Email { get; set; }
} public class RegisterUserHandler : ICommandHandler<RegisterUserCommand>
{
private readonly IRegisterUserService _registerUserService;
private readonly IUserRepository _userRepository; public void Handle(RegisterUserCommand command)
{
User user = _registerUserService.RegisterNewUser(command.Name, command.Password, command.Email);
_userRepository.Add(user);
}
}
这种架构风格带来了大量的代码工作,就是需要定义很多Command。CommandBus的具体实现就是运用了订阅/发布,即一个Command发送过来,系统会去找对应的CommandHandler,这样的代码写起来会显示更干净。
CQRS不是一个让你觉得是多么炫丽的架构,他的这种复杂性其实也是合理的,因为他是为了解决数据显示的复杂性。
接下来我就说说ES。什么是ES?全称是Event Sourcing,事件源。在未用ES之前,数据库中保存的聚合只是最后一次完整状态的数据,他不能反应聚合的历史变迁,除非你使用了其他的方式。还记得之前我稍微说了一下事件驱动吗?用了ES,必然要有事件驱动的(目前为止我还没有其他好的方式),而且还要接受最终一致性。什么是最终一致性?后面再说吧。还是用代码演示,在这里还是用户注册,为了方便这里用户密码就先不加密了,领域内的代码大致就这些
public class UserCreated : IDomainEvent
{
public UserCreated(string name, string password)
{
this.Name = name;
this.Password = password;
} public string SourceId { get; set; }
public int Version { get; set; } public string Name { get; private set; }
public string Password { get; private set; }
} public class User : IAggregateRoot
{
private readonly IList<IDomainEvent> _uncommittedEvents = new List<IDomainEvent>();
IEnumerable<IDomainEvent> IAggregateRoot.Events
{
get { return this._uncommittedEvents; }
} public User(string id)
{
this.Id = id;
} public User(string name, string password)
: this(Guid.NewGuid().ToString())
{
OnUserCreated(new UserCreated(name, password));
} private void OnUserCreated(UserCreated @event)
{
@event.SourceId = this.Id;
@event.Version = this.Version + 1; Handler(@event); this.Version = @event.Version;
_uncommittedEvents.Add(@event);
} private void Handle(UserCreated @event)
{
this.Name = @event.Name;
this.Password = @event.Password;
} void IAggregateRoot.LoadFrom(IEnumerable<IDomainEvent> events)
{
foreach (var @event in events) {
Handle(@event);
this.Version = @event.Version;
}
} public string Id { get; private set; }
public int Version { get; private set; } public string Name { get; private set; }
public string Password { get; private set; }
} public class IRepository
{
T Get<T>(string id) where T : class, IAggregateRoot;
void Save<T>(T aggregate, string commandId) where T : class, IAggregateRoot;
}
不知道上面的代码你能不能够大致明白。在这里仓储的功能更为有限,只有获取和保存聚合。当new一个user时会产生个事件,同时为事件记录一个版本号,聚合会得到最终的版本号,而且状态的修改是由事件驱动的。这个时候我们还看不出来事件的作用。别急,简单看下仓储的实现。保存聚合到底发生了什么
public class SourcedEvent
{
public SourcedEvent(string aggregateId, string aggregateName, int version)
{
this.AggregateId = aggregateId;
this.AggregateName = aggregateName;
this.Version = version;
} public string AggregateId { get; private set; }
public string AggregateName { get; private set; }
public int Version { get; private set; }
public string Payload { get; set; }
public string CorrelationId { get; set; }
} public class EventSourcedRepository : IRepository
{
public void Save<T>(T aggregate, string commandId) where T : class, IAggregateRoot
{
var events = aggregate.Events
.Select(@event => new SourcedEvent(aggregate.Id, typeof(T).Name, @event.Version) {
CorrelationId = commandId,
Payload = _serializer.Serialize(@event)
}).ToArray(); using (var connection = new SqlConnection()) {
using (var trans = connection.BeginTransaction()) {
try {
foreach (var @event in events) {
//TODO添加事件sql
}
trans.Commit();
}
catch (Exception) {
trans.RollBack();
throw;
}
}
}
_eventBus.Publish(aggregate.Events);
}
}
你会看到此时保存的仅仅是事件,持久化成功了,会将事件发布出去。这样C端的写数据库设计可以简单到只需要记录Events的一张表。而且最大的好处在于只会对event进行insert,还是就是他的存储介质不一定就需要db,甚至文本文件都行(为每个聚合创建一个文件,然后将事件追加,多么简单),想想都会兴奋,有点颠覆吧。
然后你再写个同步读数据库的EventHandler,还是再贴上代码,已经写了这么多了,不在乎再写一个了
public class UserDataSyncHandler : IEventHandler<UserCreated>
{
public void Handle(UserCreated @event)
{
string sql = string.Format("insert user(id, name, password) values('{0}','{1}','{2}')",
@event.SourceId, @event.Name, @event.Password);
}
}
至此,大致做法已经介绍完了。在这过程中你会发现用了CQRS+ES架构,可以完全抛弃ORM了,喜欢写sql的伙伴们可能会更兴奋,也许会和我一样想说“ORM我早就看你有点不爽了”,呵呵。还要说明一下前面所说的最终一致性,就是C端的事件持久化完时此时Q端的数据并没有同步过来,会存在一点延迟,但这种延迟不会太久,甚至会感觉不到。到了这里我还要有一个感触就是有了这样的架构去实现DDD,你还认为聚合模型就是数据模型吗?或者说他俩是同胞兄弟吗?
最后再展示下如何通过事件还原聚合,还是上代码吧,谁让我如此喜欢用代码来描述呢
public class EventSourcedRepository : IRepository
{
public T Get<T>(string id) where T : class, IAggregateRoot
{
IEnumerable<IDomainEvent> events;
using (var connection = new SqlConnection()) {
//TODO聚合名称和聚合ID取出事件并对版本号进行升序
} var aggregate = (T)Activator.CreateInstance(typeof(T), id);
aggregate.LoadFrom(events); return aggregate;
}
}
这样聚合就可以还原到最后一次的状态了。就像以前的电影胶片一样,每个事件对应着一个画面,放完了也就完了。
通过上面的介绍,你应该会了大致的了解了,现在来看这张图估计你就不会觉得有多么高大上了(先跳过wcf)
CQRS+ES的结合带来了很大的亮点,但是要应用考虑的会很多,复杂度也会很大,如果同步数据,如果事件丢了怎么办?产生的事件执行顺序跟我们的预期不一样怎么办?遇到并发怎么办?实体的id生成策略等等好多问题。有了问题自己的知识范围也会扩大和提高。总之,CQRS+ES可讨论的太多了,我也无法一一列举。就先写到这儿了,这一篇应该是这周最长的一篇了,明天周末了,歇两天。
用CQRS+ES实现DDD的更多相关文章
-
CQRS\ES架构介绍
大家好,我叫汤雪华.我平时工作使用Java,业余时间喜欢用C#做点开源项目,如ENode, EQueue.我个人对DDD领域驱动设计.CQRS架构.事件溯源(Event Sourcing,简称ES). ...
-
CQRS+ES项目解析-Diary.CQRS
在<当我们在讨论CQRS时,我们在讨论些神马>中,我们讨论了当使用CQRS的过程中,需要关心的一些问题.其中与CQRS关联最为紧密的模式莫过于Event Sourcing了,CQRS与ES ...
-
CQRS/ES框架调研
1.Enode一个C#写的CQRS/ES框架,由汤雪华设计及实现,github上有相关源码,其个人博客上有详细的孵化.设计思路.版本迭代及最新的完善: 2.axon framwork,java编写,网 ...
-
CQRS+ES项目解析-Equinox
今天我们来分析另一个开源的CQRS+ES项目:Equinox.该项目可以在github上下载并直接本地运行,项目地址:https://github.com/EduardoPires/EquinoxPr ...
-
一款不错的 Go Server/API boilerplate,使用 K8S+DDD+CQRS+ES+gRPC 最佳实践构建
Golang API Starter Kit 该项目的主要目的是使用最佳实践.DDD.CQRS.ES.gRPC 提供样板项目设置. 为开发和生产环境提供 kubernetes 配置.允许与反映生产的 ...
-
分享一个CQRS/ES架构中基于写文件的EventStore的设计思路
最近打算用C#实现一个基于文件的EventStore. 什么是EventStore 关于什么是EventStore,如果还不清楚的朋友可以去了解下CQRS/Event Sourcing这种架构,我博客 ...
-
Actor-ES框架:Ray
并发 1. 并发和并行 并发:两个或多个任务在同一时间段内运行.关注点在任务分割. 并行:两个或多个任务在同一时刻同时运行.关注点在同时执行. 本文大多数情况下不会严格区分这两个概念,默认并发就是指并 ...
-
一系列令人敬畏的.NET核心库,工具,框架和软件
内容 一般 框架,库和工具 API 应用框架 应用模板 身份验证和授权 Blockchain 博特 构建自动化 捆绑和缩小 高速缓存 CMS 代码分析和指标 压缩 编译器,管道工和语言 加密 数据库 ...
-
DDD创始人Eric Vans:要实现DDD原始意图,必须CQRS+Event Sourcing架构
http://www.infoq.com/interviews/Technology-Influences-DDD# 要实现DDD(domain drive design 领域驱动设计)原始意图,必 ...
随机推荐
-
android官方下拉刷新控件SwipeRefreshLayout的使用
可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介:SwipeRefreshLayout组件只 ...
-
Java多线程 2 线程的生命周期和状态控制
一.线程的生命周期 线程状态转换图: 1.新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态.处于新生状态的线程有自己的内存空间,通过调用start方法进入就 ...
- Java数据结构的特点
-
更改ubuntu mysql data目录位置
很多时候,mysql的数据会非常大,数据默认放在/var/lib/mysql,由于/var所划分的空间不够大,所以我们需要将mysql数据存放路径修改一下,放到大分区里面,以便可以应付mysql数据增 ...
-
C++学习之嵌套类和局部类
C++学习之嵌套类和局部类 局部类 在一个函数体内定义的类称为局部类. 局部类中只能使用它的外围作用域中的对象和函数进行联系,因为外围作用域中的变量与该局部类的对象无关.在定义局部类时需要注意:局部类 ...
-
oracle11g dataguard 完全手册(转)
转自:http://www.cnblogs.com/tippoint/archive/2013/04/18/3029019.html 一.前言: 网络上关于dataguard的配置文章很多,但是很 ...
-
Google SwipeRefreshLayout(Goolge官方下拉刷新控件)尝鲜
前天Google官方终于出了Android刷新控件——SwipeRefreshLayout. 使用前先需要将android.support.v4.jar升级到19.1.升级后,可能会出现SDK版本与A ...
-
mysql_study_5
代码 mysql> CREATE TABLE shop ( ) UNSIGNED ZEROFILL ' NOT NULL, ) DEFAULT '' NOT NULL, ,) DEFAULT ' ...
-
20155219 2016-2017-2 《Java程序设计》第8周学习总结
20155219 2016-2017-2 <Java程序设计>第8周学习总结 教材学习内容总结 通用API 日志API 1.java.util.logging包提供了日志功能相关类与接口, ...
-
9.22 开课第十九天(window对象)
DOM:文档对象模型 --树模型 文档:标签文档,对象:文档中每个元素对象,模型:抽象化的东西 一:window: 属性(值或者子对象): opener:打开当前窗口的源窗口,如果当前窗口是首次启动浏 ...