• 手撸一套纯粹的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

    如果代码中有错误或不合适的地方,请在评论中指出,谢谢支持。

    参考文档

  • 相关阅读:
    vue中Axios的封装和API接口的管理
    解决Vue报错:TypeError: Cannot read property 'scrollHeight' of undefined
    JS-scrollTop、scrollHeight、clientTop、clientHeight、offsetTop、offsetHeight的理解
    理解Vue中的ref和$refs
    理解Vue中的nextTick
    CSS——overflow的参数以及使用
    JS数据结构——队列
    Vue中实现聊天窗口overflow:auto自动滚动到底部,实现显示当前最新聊天消息
    Vue中无法检测到数组的变动
    JS-观察者模式
  • 原文地址:https://www.cnblogs.com/youring2/p/10991338.html
Copyright © 2020-2023  润新知