关于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. Dubbo封装rest服务返回结果

    由于Dubbo服务考虑到一个是给其他系统通过RPC调用,另外一个是提供HTTP协议本身系统的后台管理页面,因此Dubbo返回参数在rest返回的时候配置拦截器进行处理. 在拦截器中,对返回参数封装成如 ...

  2. 关于ajax中return并不能作为方法的返回值

    接下来关于ajax中的return值最后没有办法是方法的最终返回值问题 login(username,password) { console.log("进入方法"); $.ajax ...

  3. Linux常用操作详解

    第1章 Linux命令基础 1.1 习惯 操作前备份,操作后检查 1.2 简单目录结构 一切从根开始,与windows不同 1.3 规则 [root@znix ~]# [用户名@主机名 你在哪]# 1 ...

  4. js正则匹配获取文件名

    //获取文件名,不带后缀 var file_name=file_path.replace(/(.*\/)*([^.]+).*/ig,"$2"); //获取文件后缀 1.var Fi ...

  5. zblog去除底部版权信息 “请勿修改或删除主题版权及作者信息”

    场景:使用了免费模板,但底部带作者版权.删除版权信息的代码后访问前台弹窗:请勿修改或删除主题版权及作者信息... 1. 删除版权信息代码 使用notepad++搜索功能,搜索版权信息:如ABC,找到相 ...

  6. 投资20万研发的JFinal项目《旅游线路营销管理系统》准备公开课中

    18年初上线了一套旅游营销管理系统,目前给几个合作客户内测试用,是基于JFinal研发的一套旅游行业旅游线路批发零售系统(SAAS)版. 系统终端: PC后台管理分:总部.线路批发商.旅行社门店.个人 ...

  7. fpathconf

    http://pubs.opengroup.org/onlinepubs/009695399/functions/pathconf.html

  8. Azure 门户使用概览

    Azure 门户是管理 Azure 云平台的核心工具,用户可以在其中预配和管理 Azure 资源.本教程将帮助你熟悉Azure管理门户,包括一些关键功能的介绍,并演示了如何通过 Azure 门户创建虚 ...

  9. Python3获取大量电影信息:调用API

    实验室这段时间要采集电影的信息,给出了一个很大的数据集,数据集包含了4000多个电影名,需要我写一个爬虫来爬取电影名对应的电影信息. 其实在实际运作中,根本就不需要爬虫,只需要一点简单的Python基 ...

  10. JAVA小游戏之两个物体碰撞产生的碰撞检测

    首先必须了解两个物体,在移动时,会有怎样的效果,比如沪我们小时候耍过的坦克大战.看起来很简单,但是写起代码来,复杂的要多: 下面举个例子: // 构造一个新的 Rectangle,其左上角的坐标为 ( ...