关于CQRS,在实现上有很多差异,这是因为CQRS本身很简单,但是它犹如潘多拉魔盒的钥匙,有了它,读写分离、事件溯源、消息传递、最终一致性等都被引入了框架,从而导致CQRS背负了太多的混淆。本文旨在提供一套简单的CQRS实现,不依赖于ES、Messaging等概念,只关注CQRS本身。

CQRS的本质是什么呢?我的理解是,它分离了读写,为读写使用不同的数据模型,并根据职责来创建相应的读写对象;除此之外其它任何的概念都是对CQRS的扩展。

下面的伪代码将展示CQRS的本质:

使用CQRS之前:

CustomerService

void MakeCustomerPreferred(CustomerId)
Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

使用CQRS之后:

CustomerWriteService

void MakeCustomerPreferred(CustomerId)
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

CustomerReadService

Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()

Query

查询(Query): 返回结果,但是不会改变对象的状态,对系统没有副作用。

查询的实现比较简单,我们首先定义一个只读的仓储:

public interface IReadonlyBookRepository
{
IList<BookItemDto> GetBooks(); BookDto GetById(string id);
}

然后在Controller中使用它:

public IActionResult Index()
{
var books = readonlyBookRepository.GetBooks(); return View(books);
}

Command

命令(Command): 不返回任何结果(void),但会改变对象的状态。

命令代表用户的意图,包含业务数据。

首先定义ICommand接口,该接口不含任何方法和属性,仅作为标记来使用。

public interface ICommand
{ }

与Command对应的有一个CommandHandler,Handler中定义了具体的操作。

public interface ICommandHandler<TCommand>
where TCommand : ICommand
{
void Execute(TCommand command);
}

为了能够封装Handler的定位,我们还需要定一个ICommandHandlerFactory:

public interface ICommandHandlerFactory
{
ICommandHandler<T> GetHandler<T>() where T : ICommand;
}

ICommandHandlerFactory的实现:

public class CommandHandlerFactory : ICommandHandlerFactory
{
private readonly IServiceProvider serviceProvider; public CommandHandlerFactory(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
} public ICommandHandler<T> GetHandler<T>() where T : ICommand
{
var types = GetHandlerTypes<T>();
if (!types.Any())
{
return null;
} //实例化Handler
var handler = this.serviceProvider.GetService(types.FirstOrDefault()) as ICommandHandler<T>;
return handler;
} //这段代码来自Diary.CQRS项目,用于查找Command对应的CommandHandler
private IEnumerable<Type> GetHandlerTypes<T>() where T : ICommand
{
var handlers = typeof(ICommandHandler<>).Assembly.GetExportedTypes()
.Where(x => x.GetInterfaces()
.Any(a => a.IsGenericType && a.GetGenericTypeDefinition() == typeof(ICommandHandler<>)))
.Where(h => h.GetInterfaces()
.Any(ii => ii.GetGenericArguments()
.Any(aa => aa == typeof(T)))).ToList(); return handlers;
}

然后我们定义一个ICommandBus,ICommandBus通过Send方法来发送命令和执行命令。定义如下:

public interface ICommandBus
{
void Send<T>(T command) where T : ICommand;
}

ICommandBus的实现:

public class CommandBus : ICommandBus
{
private readonly ICommandHandlerFactory handlerFactory; public CommandBus(ICommandHandlerFactory handlerFactory)
{
this.handlerFactory = handlerFactory;
} public void Send<T>(T command) where T : ICommand
{
var handler = handlerFactory.GetHandler<T>();
if (handler == null)
{
throw new Exception("未找到对应的处理程序");
} handler.Execute(command);
}
}

我们来定一个新增命令CreateBookCommand:

public class CreateBookCommand : ICommand
{
public CreateBookCommand(CreateBookDto dto)
{
this.Dto = dto;
} public CreateBookDto Dto { get; set; }
}

我不知道这里直接使用DTO对象来初始化是否合理,我先这样来实现

对应CreateBookCommand的Handler如下:

public class CreateBookCommandHandler : ICommandHandler<CreateBookCommand>
{
private readonly IWritableBookRepository bookWritableRepository; public CreateBookCommandHandler(IWritableBookRepository bookWritableRepository)
{
this.bookWritableRepository = bookWritableRepository;
} public void Execute(CreateBookCommand command)
{
bookWritableRepository.CreateBook(command.Dto);
}
}

当我们在Controller中使用时,代码是这样的:

[HttpPost]
public IActionResult Create(CreateBookDto dto)
{
dto.Id = Guid.NewGuid().ToString("N");
var command = new CreateBookCommand(dto);
commandBus.Send(command); return Redirect("~/book");
}

UI层不需要了解Command的执行过程,只需要将命令通过CommandBus发送出去即可,对于前端的操作也很简洁。

该实例的完整代码在github上,感兴趣的朋友请移步>>https://github.com/qifei2012/sample_cqrs

如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。

参考文档

手撸一套纯粹的CQRS实现的更多相关文章

  1. 2w字长文!手撸一套 Java 基础面试题

    Java 基础篇 Java 有哪些特点 并发性的: 你可以在其中执行许多语句,而不必一次执行它 面向对象的:基于类和面向对象的编程语言. 独立性的: 支持一次编写,到处运行的独立编程语言,即编译后的代 ...

  2. php手撸轻量级开发(一)

    聊聊本文内容 之前讲过php简单的内容,但是原生永远是不够看的,这次用框架做一些功能性的事情. 但是公司用自己的框架不能拿出来,用了用一些流行的框架比如tp,larveral之类的感觉太重,CI也不顺 ...

  3. Haskell手撸Softmax回归实现MNIST手写识别

    Haskell手撸Softmax回归实现MNIST手写识别 前言 初学Haskell,看的书是Learn You a Haskell for Great Good, 才刚看到Making Our Ow ...

  4. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  5. 手撸Router,还要啥Router框架?react-router/vue-router躺一边去

    有没有发现,在大家使用React/Vue的时候,总离不开一个小尾巴,到哪都得带着他,那就是react-router/vue-router,而基于它们的第三方框架又出现很多个性化约定和扩展,比如nuxt ...

  6. 使用Java Socket手撸一个http服务器

    原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...

  7. 【手撸一个ORM】MyOrm的使用说明

    [手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...

  8. 康少带你手撸orm

    orm 什么是orm? 对象关系映射: 一个类映射成一张数据库的表 类的对象映射成数据库中的一条条数据 对象点数据映射成数据库某条记录的某个值 优点:不会写sql语句的程序员也可以很6的操作sql语句 ...

  9. 手写一套迷你版HTTP服务器

    本文主要介绍如何通过netty来手写一套简单版的HTTP服务器,同时将关于netty的许多细小知识点进行了串联,用于巩固和提升对于netty框架的掌握程度. 服务器运行效果 服务器支持对静态文件css ...

随机推荐

  1. springboot 学习笔记(三)

    (三)用jar包启动springboot项目 1.首先需要在pom文件中添加依赖,spring-boot-starter-parent包含有打包的默认配置,如果要修改的话要可以进行重新定义,具体内容参 ...

  2. 从typeof()说起

    本文也同步发表在我的公众号“我的天空” 首先我们先思考一下,执行下列语句分别会显示什么? alert(typeof(Array)); alert(typeof(Array())); 我们进入正题! 在 ...

  3. windows下vue-cli及webpack 构建网站(一)环境安装

    1.安装Node.js,node.js安装包及源码下载地址为:https://nodejs.org/en/download/. 2.安装npm,由于新版的nodejs已经集成了npm,所以之前npm也 ...

  4. nodejs学习8:windows连接mongodb出现的错误解决办法

    今天遇到了在windows下连接mongodb错误的情况,因为之前安装是正常的,而重启的电脑之后就再也连接不上.于是在群里求助了下,无果,查阅了官网的英文文档,终于有些眉目了,故此一记. 先吐槽下命令 ...

  5. JS根据选择的日期计算年龄

    本例中用的是easyUI的datebox $('#cal_birthday').datebox({ onSelect: function(date){ //根据选则的日期计算年龄 //alert(da ...

  6. Android RecyclerView使用GridLayoutManager导致间隙变大的问题

    我用recyclerView的时候设置LayoutManager为Grid,添加decoration为Grid,作为二级列表时,多次点击一级列表来跳转的时候,两张图之间的间隙在逐渐变大,后来发现是因为 ...

  7. PHP中调用SVN命令更新网站方法(解决文件名包含中文更新失败的问题)

    想说写一个通过网页就可以执行 SVN 升级的程序,结果并不是我想得那样简单,有一些眉角需要注意的说. 先以 Apache 的用户帐号执行 SVN checkout,这样 Apache 才有 SVN 的 ...

  8. Linux安装loadrunner负载机

    1.loadrunner下载地址:http://download.csdn.net/download/intel80586/9542271或者其他资源 2.首先用rpm -qa|grep -i c++ ...

  9. 使用tensorflow object_detection API训练自己的数据遇到的问题及解决方法

    1.Windows下出现找不到object_detection包的问题. 解决方法 在Anaconda3\soft\Lib\site-packages新建一个pth文件,将PedestrianDete ...

  10. Codeforces Round #321 (Div. 2) E Kefa and Watch (线段树维护Hash)

    E. Kefa and Watch time limit per test 1 second memory limit per test 256 megabytes input standard in ...