手撸一套纯粹的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 ...
随机推荐
- 提升Java代码质量(二)
Item5:消除过期对象的引用 JVM为我们实现了GC(垃圾回收)的功能,让我们从手工管理内存中解放了出来,这固然很好,但并不意味着我们就再也不需要去考虑内存管理的事情了;我们用简单的栈实现的例子来解 ...
- 一条shell统计代码行数
Xcode统计代码,用shell命令即可,非常简单.打开终端,进入你的工程目录,执行下列代码 find . -name "*.m" -or -name "*.h" ...
- Servlet中的初始化参数、上下文参数、以及@Resource资源注入
配置初始化参数.上下文参数.以及使用@Resource注解进行资源注入,目的是为了降低代码的耦合度.当项目需求进行变更的时候,不需要反复更改源代码,只需更改web.xml文件即可. 一:Servlet ...
- 关于验证码在IE中不刷新的快速解决方法
今天在做验证码的时候发现在IE中,验证码不会刷新,而谷歌等其他浏览器没有问题,所以我想到应该是缓存问题,因为IE默认的设置是如果访问地址没变化就不会去获取而是加载缓存中的内容 所以解决方案就是在验证码 ...
- 数据库操作----找了MySQL和SQL Sever两个的基础语句
这是MySQL的基本操作: 1 登入数据库:mysql -uroot -p+密码 (SQL Sever登入: osql -U 用户名 -P 密码) 显示已存在的数据库:show databases; ...
- 获取cell中的button在整个屏幕上的位置
编写cell中得button点击事件 - (IBAction)showButtonClick:(id)sender { UIButton *button = (UIButton *)sender; U ...
- Eclipse中添加MyEclipse插件
下载准备: 下载myeclipse7.0:http://downloads.myeclipseide.com/downloads/products/eworkbench/7.0M1/MyEclipse ...
- UVA225 Golygons 黄金图形(dfs+回溯)
剪枝1:在同一个维度上的点具有相同的奇偶性,如果奇数数量只有奇数个那么一定不能返回原点. 剪枝2:当前位置怎么也走不回去. 3:沿途判断障碍即可. 在oj上提交0.347s,最快的0.012s,应该有 ...
- python_96_类的继承1
#面向对象3大特性:封装,多态,继承 # 继承可节省内存,减少代码 class People(): def __init__(self,name,age): self.Name=name self.A ...
- Django 的母板及布局(Bootstrap)
title: Django 的母板及布局(Bootstrap) tags: Django --- Django 的母板及布局(Bootstrap) Django 的母板是作为公共的部分,其他的页面都能 ...