关于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): 返回结果,但是不会改变对象的状态,对系统没有副作用。
查询的实现比较简单,我们首先定义一个只读的仓储:
public interface IReadonlyBookRepository { IList<BookItemDto> GetBooks(); BookDto GetById(string id); }然后在Controller中使用它:
public IActionResult Index() { var books = readonlyBookRepository.GetBooks(); return View(books); }命令(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
如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。