手撸一套纯粹的CQRS实现
关于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
如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。
参考文档
- https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html
- http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/
手撸一套纯粹的CQRS实现的更多相关文章
- 2w字长文!手撸一套 Java 基础面试题
Java 基础篇 Java 有哪些特点 并发性的: 你可以在其中执行许多语句,而不必一次执行它 面向对象的:基于类和面向对象的编程语言. 独立性的: 支持一次编写,到处运行的独立编程语言,即编译后的代 ...
- php手撸轻量级开发(一)
聊聊本文内容 之前讲过php简单的内容,但是原生永远是不够看的,这次用框架做一些功能性的事情. 但是公司用自己的框架不能拿出来,用了用一些流行的框架比如tp,larveral之类的感觉太重,CI也不顺 ...
- Haskell手撸Softmax回归实现MNIST手写识别
Haskell手撸Softmax回归实现MNIST手写识别 前言 初学Haskell,看的书是Learn You a Haskell for Great Good, 才刚看到Making Our Ow ...
- 第二篇-用Flutter手撸一个抖音国内版,看看有多炫
前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽, 先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...
- 手撸Router,还要啥Router框架?react-router/vue-router躺一边去
有没有发现,在大家使用React/Vue的时候,总离不开一个小尾巴,到哪都得带着他,那就是react-router/vue-router,而基于它们的第三方框架又出现很多个性化约定和扩展,比如nuxt ...
- 使用Java Socket手撸一个http服务器
原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...
- 【手撸一个ORM】MyOrm的使用说明
[手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...
- 康少带你手撸orm
orm 什么是orm? 对象关系映射: 一个类映射成一张数据库的表 类的对象映射成数据库中的一条条数据 对象点数据映射成数据库某条记录的某个值 优点:不会写sql语句的程序员也可以很6的操作sql语句 ...
- 手写一套迷你版HTTP服务器
本文主要介绍如何通过netty来手写一套简单版的HTTP服务器,同时将关于netty的许多细小知识点进行了串联,用于巩固和提升对于netty框架的掌握程度. 服务器运行效果 服务器支持对静态文件css ...
随机推荐
- 7.Solution的Build、Rebuild和Clean
大家好,我是原文,这篇随笔是对原文的翻译以及自己的体会. 做程序员没追求的话是永远找不到女朋友的,当然有追求也找不到,这个先不提,好在有追求的时候我是充实而且开心的.现在我们的问题是,每天调试项目,在 ...
- springIOC+Mysql+springmvc事务测试题总结
1.关于http1.1和1.0的长连接和短连接 两个都支持长连接和短连接 http1.0默认为短连接,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接 http1.1 ...
- logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?><!--该日志将日志级别不同的log信息保存到不同的文件中--&g ...
- ECShop怎么首页调用文章列表
举例如首页调用方法:1.先打开index.php文件找到以下代码:$smarty->assign('new_articles', index_get_new_articles()); // 最新 ...
- <Android 应用 之路> 聚合数据SDK
聚合数据介绍 聚合数据是一个为智能手机开发者,网站站长,移动设备开发人员及图商提供原始数据API服务的综合性云数据平台.包含手机聚合,网站聚合,LBS聚合三部分,其功能类似于Google APIS.[ ...
- 【整站源码分享】分享一个JFinal3.4开发的整站源码,适合新手学习
分享这个源码是14年开发上线的<威海创业者>站点的全套整站源码,前后端都在一个包里.当时开发使用的是JFinal1.4,最近改成了JFinal3.4.使用的JSP做的页面.有一定的参考价值 ...
- 通过Jenkins调用自动部署war包及jar包到服务器上的Shell脚本
1)部署war包#!/bin/bashif [ id>0];then echo"stopproject" kill −9 idelse echo "project ...
- 安装新版REDIS
http://redis.io/ # wget http://download.redis.io/redis-stable.tar.gz tar zxvf redis-stable.tar.gz -C ...
- SAP标准培训课程C4C10学习笔记(二)第二单元
第二单元目录: SAP Cloud for Customer的项目实施分为4个阶段: 这四个阶段的详细介绍在SAP社区上这篇博文里: SAP Hybrid Project implementation ...
- VC 对话框设置背景颜色和图片
改变背景颜色,有两种方法: 1.在app的初始化函数中调用:void SetDialogBkColor( COLORREF clrCtlBk = RGB(192, 192, 192), COLORRE ...