前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手撸一套纯粹的CQRS实现

手撸一套纯粹的CQRS实现

作者头像
拓荒者-NET
发布2019-09-24 15:16:08
5580
发布2019-09-24 15:16:08
举报

关于CQRS,在实现上有很多差异,这是因为CQRS本身很简单,但是它犹如潘多拉魔盒的钥匙,有了它,读写分离、事件溯源、消息传递、最终一致性等都被引入了框架,从而导致CQRS背负了太多的混淆。本文旨在提供一套简单的CQRS实现,不依赖于ES、Messaging等概念,只关注CQRS本身。

CQRS的本质是什么呢?我的理解是,它分离了读写,为读写使用不同的数据模型,并根据职责来创建相应的读写对象;除此之外其它任何的概念都是对CQRS的扩展。

下面的伪代码将展示CQRS的本质:

使用CQRS之前:

CustomerService

代码语言:javascript
复制
void MakeCustomerPreferred(CustomerId) 
Customer GetCustomer(CustomerId) 
CustomerSet GetCustomersWithName(Name) 
CustomerSet GetPreferredCustomers() 
void ChangeCustomerLocale(CustomerId, NewLocale) 
void CreateCustomer(Customer) 
void EditCustomerDetails(CustomerDetails)

使用CQRS之后:

CustomerWriteService

代码语言:javascript
复制
void MakeCustomerPreferred(CustomerId) 
void ChangeCustomerLocale(CustomerId, NewLocale) 
void CreateCustomer(Customer) 
void EditCustomerDetails(CustomerDetails)

CustomerReadService

代码语言:javascript
复制
Customer GetCustomer(CustomerId) 
CustomerSet GetCustomersWithName(Name) 
CustomerSet GetPreferredCustomers()

Query

查询(Query): 返回结果,但是不会改变对象的状态,对系统没有副作用。

查询的实现比较简单,我们首先定义一个只读的仓储:

代码语言:javascript
复制
public interface IReadonlyBookRepository
{
    IList<BookItemDto> GetBooks();

    BookDto GetById(string id);
}

然后在Controller中使用它:

代码语言:javascript
复制
public IActionResult Index()
{
    var books = readonlyBookRepository.GetBooks();

    return View(books);
}

Command

命令(Command): 不返回任何结果(void),但会改变对象的状态。

命令代表用户的意图,包含业务数据。

首先定义ICommand接口,该接口不含任何方法和属性,仅作为标记来使用。

代码语言:javascript
复制
public interface ICommand
{
    
}

与Command对应的有一个CommandHandler,Handler中定义了具体的操作。

代码语言:javascript
复制
public interface ICommandHandler<TCommand>
    where TCommand : ICommand
{
    void Execute(TCommand command);
}

为了能够封装Handler的定位,我们还需要定一个ICommandHandlerFactory:

代码语言:javascript
复制
public interface ICommandHandlerFactory
{
    ICommandHandler<T> GetHandler<T>() where T : ICommand;
}

ICommandHandlerFactory的实现:

代码语言:javascript
复制
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方法来发送命令和执行命令。定义如下:

代码语言:javascript
复制
public interface ICommandBus
{
    void Send<T>(T command) where T : ICommand;
}

ICommandBus的实现:

代码语言:javascript
复制
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:

代码语言:javascript
复制
public class CreateBookCommand : ICommand
{
    public CreateBookCommand(CreateBookDto dto)
    {
        this.Dto = dto;
    }

    public CreateBookDto Dto { get; set; }
}

我不知道这里直接使用DTO对象来初始化是否合理,我先这样来实现

对应CreateBookCommand的Handler如下:

代码语言:javascript
复制
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中使用时,代码是这样的:

代码语言:javascript
复制
[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

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

参考文档

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Query
  • Command
    • 参考文档
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档