手擼一套純粹的CQRS實現

關於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

查詢(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

命令(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

若是代碼中有錯誤或不合適的地方,請在評論中指出,謝謝支持。

參考文檔

相關文章
相關標籤/搜索