關於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
如果代碼中有錯誤或不合適的地方,請在評論中指出,謝謝支持。
