CQRS很难吗?(译文:底部有原文地址)

很久没来这里了,今天登上来发现界面风格变化很大,并且博客也开始支持markdown,愈来愈先进了。那么就借着这个势头分享下最近一直在研究的DDD-CQRS。html

有些人说“CQRS很难吗?”redis

其实它和微服务结合起来才是王道!数据库

是吗?好吧,我以前也一直是这么认为的!可是当我开始编写使用CQRS的第一件软件做品时,一切都改变了。我发现他一点都不复杂。并且我认为在大型团队中保持这种编程行为将变得更容易。编程

我也曾思考为何人们广泛认为CQRS很难,后来我想到是为何了。由于有不少规则!人们讨厌规则,现实社会中已经不少了,因此不少人沉迷于虚拟世界之中。规则让咱们不舒服,由于咱们必须遵照它。在这篇博文中,我将从几方面论证这些规则是容易理解的。缓存

通常来讲,咱们认为CQRS是读写分离架构的一种实现方式。当我以这种方法来理解的时候我发如今简单的CQS实现和真实的CQRS之间仍是有几个很重要的步骤须要区分对待的。这些步骤介绍了我以前提到的规则。markdown

咱们的旅程从下面这幅图开始:网络

上面是一张咱们都很熟悉的经典N层架构图。若是往其中加入一些CQS的话,咱们就简单地实现了将业务逻辑分离为命令和查询:session

若是你在维护一个旧系统这多是最难的一步(由于老代码基本上都是意大利面条似的)。同时这一步也多是最有效果的,由于你从中对本身的代码有了一个总体的梗概。架构

下面先来介绍下命令(Command)和查询(Query)的定义吧!框架

首先,发送命令是惟一改变系统状态的方式。Commands对全部系统的变动都要负责。若是没有command,系统的状态就一直保持不变!Command不该该返回任何值。我使用两个类来实现:Command和CommandHandler。Command仅仅是被CommandHandler是用来做为一些操做的输入参数。在我这里,command仅仅是领域模型中调用的一系列特定操做。

query至关于读操做。经过读取系统的状态,过滤,聚合,转换数据,最后以某种特定的格式传输。它能够被执行屡次而且不会影响到系统的状态。我一般在一个类中使用多个execute(...)方法来实现,但我如今却认为将Query和QueryHandler、QueryExecutor分开来说或许是对的。

回到上面的那张图,我须要澄清一些事;我私自加进去了额外的变化,就是Model变成了Domain Model。model表明了数据的容器,而domain model则包含了复杂的商业逻辑。这点变化对于咱们感兴趣的架构来讲没什么直接的影响,可是值得注意的是因为command接管了修改系统状态的职责,主要的复杂性都集中在了Domain Model中。

稍后你就会发现Domain Model对于写模型来讲颇有用,可是对于读来说却表现不是很好。

咱们可使用这种分离模型,经过ORM映射来构造查询,可是在某些场景,尤为是当ORM遇到负载失衡的时候,下面这个架构可能会更合适:

如今咱们成功地在逻辑层面分离了Command和Query,可是他们还在共享同一个数据库。这意味着实际上读模型在使用的是DB的物化视图(也可经过数据库层面的读写分离代替视图)。当咱们的系统不须要解决性能问题,并且咱们记得在写模型变动的时候更新咱们的查询的时候,这个解决方案还能够。

下一步是介绍完整的分离数据模型:

CQRS != Event Sourcing

Event Sourcing(事件溯源)常常伴随着CQRS出现。ES的的定义很简单:咱们的领域产生的事件即代表了系统中发生过的变化。若是咱们从刚开始就记录了整个系统并进行回放,咱们就会拿到当初系统的状态记录。能够想象下银行帐户,从刚开始的空帐户,经过回放每一个单一事务获得最终(也就是目前)的收支状况。因此,只要咱们存储了全部的事件,就能获得系统当前的状态。

可是对于CQRS来讲,领域模型究竟是怎样存储的不是特别重要,ES仅仅是其中的一个选项。(也可使用in-memory或mongo,redis等方式)

写模型

因为写模型定义了领域模型的主要职责,而且作了不少商业决定,它是系统的心脏。它体现了系统的真实状态,这些状态能够用来作出有意义的决定。

读模型

刚开始我使用写模型来构建凑合的查询,在不断的尝试以后,咱们发现这很费时间。由于咱们是工程师,优化是第二需求。咱们设计的模型在读取时将时间消耗在了关联查询上。咱们被迫预先计算出一些有报表需求的数据,这样会使它在查询时表现得很快。这颇有趣,由于咱们在这里使用到了缓存。依我看来这是对读模型最好的诠释:它就是一个合理存在的缓存。缓存在这里没有细讲,是由于咱们尚未触及到项目的发布,非功能性需求不该该过早设计。

事实上,读模型能够设计的很复杂,你可使用图数据库(相似Neoj)来存储社交网络,而使用RDBMS(关系型数据库)来存储财务数据。

设计良好的读模型是须要必定的考量的。若是你的项目不是很大,写模型已经能够胜任几乎全部的读取需求,那么设计个读模型将会致使大量的拷贝代码,这种作法无疑是在浪费时间。可是若是你的写模型存储了一系列的事件,那么你将绝不费力得从中获取到任什么时候间点上的数据,而不用将事件从零开始进行回放。这个过程被称做Eager Read Derivation,也是我认为的在CQRS最复杂的一块。读模型即如我以前所说,是另外一种形式的缓存。而Phil Karlton曾说过“在计算机科学中只有两个问题值得咱们关注:缓存的时效性和命名问题”

最终一致性

若是咱们的模型处于物理上隔离的情况,那么同步就须要花费一些时间,可是这段时间对于某些业务来说是不能耽误的。在个人项目中,若是全部的部分都运行得很正确,读模型处于不一样步的状态是能够忽略不计的。然而,咱们必须将时间上的差别性考虑进去尤为是在开发更复杂的系统时。咱们设计的UI也是为了可以及时的处理最终一致性。

咱们不得不认可即便在写模型更新的时候读模型也相应更新的状况下,用户也是难以接受老旧数据的出现。更况且咱们根部不肯定展示在用户面前的数据是不是最新的。

下面我将要讲述一些实际的代码例子:

我是怎样将CQRS引入个人项目中的?

我认为CQRS是简单的以致于不须要引入任何框架。你能够从少于100行的代码开始慢慢地实现它,当须要时再去扩展新功能。你不用作任何努力(学习新的技术),由于CQRS已经简化了软件开发。下面是个人实现:

public interface ICommand {

}

public interface ICommandHandler
    where TCommand : ICommand {
    void Execute(TCommand command);
}

public interface ICommandDispatcher {
    void Execute(TCommand command)
        where TCommand : ICommand;
}

我定义了两个接口用来描述命令和他的执行环境。这么作是由于我想保持参数的扁平化,不想要任何依赖。个人命令处理器(command handler)可以从DI容器中请求依赖,根本没有必要手动初始化它(除了在自测用例中)。实际上,ICommand接口出如今这里无非是想告诉开发人员咱们把这个类当作command来用。

public interface IQuery {

}

public interface IQueryHandler 
    where TQuery : IQuery {
    TResult Execute(TQuery query);
}

public interface Interface IQueryDispatcher {
    TResult Execute(TQuery query)
        where TQuery : IQuery;
}

这里讲IQuery接口做为返回的query结果类型。但这不是最优雅的方法,可是在编译器类型被肯定了。

public class CommandDispatcher : ICommandDispatcher {
    private readonly IDependencyResolver _resolver;

    public CommandDispatcher(IDependencyResolver resolver) {
        _resolver = resolver;
    }

    public void Execute(TCommand command)
        where TCommand : ICommand {
        if(command == null) {
            throw new ArgumentNullException("command");
        }
        var handler = _resolver.Resolver>();

        if(handler == null) {
            throw new CommandHandlerNotFoundException(typeof(TCommand));
        }

        handler.Execute(command);
     }
}

这里的CommandDispatcher相对来讲是短小的,它只有一个职责就是为command初始化合适的command handler并执行。为了不手写command注册和初始化过程,我使用了DI容器来帮我作了。可是若是你不想使用DI容器你能够本身来实现。我认为这并不难。只有一个问题,那就是通用类型是比较难定义的,这在刚开始这么作时可能有些挫败感。但这种实现使用起来已经很简单了,这里是一个简单的command和handler的例子:
 

public class SignOnCommand : ICommand {

    public AssignmentId Id { get; private set; }
    public LocalDateTime EffectiveDate { get; private set; }

    public SignOnCommand(AssignmentId assignmentId, LocalDateTime effectiveDate) {

        Id = assignmentId;
        EffectiveDate = effectiveDate;
    }
}

public class SignOnCommandHandler : ICommandHandler {

    private readonly AssignmentRepository _assignmentRepository;
    private readonly SignOnPolicyFactory _factory;

    public SignOnCommandHandler(AssignmentRepository assignmentRepository,
                                SignOnPolicyFactory factory) {
        _assignmentRepository = assignmentRepository;
        _factory = factory;
    }

    public void Execute(SignOnCommand command) {
        var assignment = _assignmentRepository.GetById(command.Id);

        if(assignment == null) {

            throw new MeaningfulDomainException("Assignment not found!");
        }

        var policy = _factory.GetPolicy();

        assignment.SignOn(command.EffectiveDate, policy);
    }
}

只须要将SignOnCommand传入dispatcher就能够了:

_commandDispatcher.Execute(new SignOnCommand(new AssignmentId(rawId), effectiveDate));

就是这么简单!惟一的区别就是它返回了指定的数据,依赖于以前定义的通用的Execute方法,返回了强类型的结果:

public class QueryDispatcher : IQueryDispatcher {
    private readonly IDependencyResolver _resolver;

    public QueryDispatcher(IDependencyResolver resolver) {
        _resolver = resolver;
    }

    public void Execute(TQuery query)
        where TQuery : IQuery {
        if(query == null) {
            throw new ArgumentNullException("query");
        }
        var handler = _resolver.Resolver>();

        if(handler == null) {
            throw new QueryHandlerNotFoundException(typeof(TQuery));
        }

        handler.Execute(query);
     }
}

这个实现是扩展性极强的。好比咱们想引入事务到comamnd dispatcher中,能够像下面这样作,无须动用任何原有的实现代码:

public class TransactionalCommandDispatcher : ICommandDispatcher {
    private readonly ICommandDispatcher _next;
    private readonly ISessionFactory _sessionFactory
;

    public TransactionalCommandDispatcher(ICommandDispatcher next,
        ISessionFactory sessionFactory) {
        _next = next;
        _sessionFactory = sessionFactory;
    }

    public void Execute(TCommand command)
        where TCommand : ICommand {
        using(var session = _sessionFactory.GetSession())
            using(var tx = session.BeginTransaction()) {

            try {
                _next.Execute(command);
                ex.Commit();
            } catch {
                tx.Rollback();
                throw;
            }
         }
     } 
}

经过使用这种“伪切面”,咱们能够简单地实现Command和Query dispatcher的扩展。

如今你明白了CQRS并不难,基本的观点已经讲的很清晰了,可是你仍是须要听从一些规则。这篇博客没有包含所有的你想知道的东西,因此我推荐你读一读下面这些文章。

参考文献:

1 CQRS Documents by Greg Young

2 Clarified CQRS by Udi Dahan

3 CQRS by Martin Fowler

4 CQS by Martin Fowler

5 “Implementing DDD” by Vaughn Vernon

原文地址:https://www.future-processing.pl/blog/cqrs-simple-architecture/

相关文章
相关标签/搜索