關於CQRS,在實現上有不少差別,這是由於CQRS自己很簡單,可是它猶如潘多拉魔盒的鑰匙,有了它,讀寫分離、事件溯源、消息傳遞、最終一致性等都被引入了框架,從而致使CQRS揹負了太多的混淆。本文旨在提供一套簡單的CQRS實現,不依賴於ES、Messaging等概念,只關注CQRS自己。html
CQRS的本質是什麼呢?個人理解是,它分離了讀寫,爲讀寫使用不一樣的數據模型,並根據職責來建立相應的讀寫對象;除此以外其它任何的概念都是對CQRS的擴展。前端
下面的僞代碼將展現CQRS的本質:git
使用CQRS以前:github
CustomerService框架
void MakeCustomerPreferred(CustomerId) Customer GetCustomer(CustomerId) CustomerSet GetCustomersWithName(Name) CustomerSet GetPreferredCustomers() void ChangeCustomerLocale(CustomerId, NewLocale) void CreateCustomer(Customer) void EditCustomerDetails(CustomerDetails)
使用CQRS以後:ide
CustomerWriteServiceui
void MakeCustomerPreferred(CustomerId) void ChangeCustomerLocale(CustomerId, NewLocale) void CreateCustomer(Customer) void EditCustomerDetails(CustomerDetails)
CustomerReadServicethis
Customer GetCustomer(CustomerId) CustomerSet GetCustomersWithName(Name) CustomerSet GetPreferredCustomers()
查詢(Query): 返回結果,可是不會改變對象的狀態,對系統沒有反作用。code
查詢的實現比較簡單,咱們首先定義一個只讀的倉儲:htm
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
若是代碼中有錯誤或不合適的地方,請在評論中指出,謝謝支持。