翻译于 2018/08/06 13:30
1 人 顶 此译文
SOME SAY: “CQRS IS HARD!”
IS IT? WELL, I USED TO THINK THE SAME! BUT WHEN I STARTED WRITING MY FIRST PIECE OF SOFTWARE USING CQRS, IT QUICKLY TURNED OUT THAT IT IS RATHER UNCOMPLICATED. WHAT IS MORE, I THINK THAT IN LONG TERM IT IS MUCH EASIER TO MAINTAIN SOFTWARE WRITTEN IN THIS WAY.
I have started to think, what is the reason that people see it as hard and complex at the beginning? I have a theory: it has rules! Entering the world with rules is always uncomfortable, we need to adjust to rules. In this post I’d like to prove that in this case those rules are quite digestible.
是吗？ 好吧，我也曾这样认为！ 但，当我开始使用 CQRS 编写我的第一个软件时，它很快就不攻自破。更为重要的是，我认为从长远来看，以这种方式维护软件更加容易。
我开始思考：为何人们在一开始时认为它是多么困难难和复杂？ 我有一个理论：它包含规则！ 进入拥有规则的世界总是不舒服的，我们需要适应这些规则。在这篇文章中，我想证明在这种情况下，这些规则是非常易于理解的。
Basically, we can say that CQRS is an implementation of Command Query Separation principle to the architecture of software. What I noticed during my work in this approach is that there is a couple of steps between simplest CQS implementation and real full-blown CQRS. I think that those steps smoothly introduce rules I have mentioned before.
While the first steps are not fulfilling my definition of CQRS (but are sometimes so called), they can still introduce some real value to your software. Each step introduces some interesting ideas that may help to structure or clean your codebase/architecture.
Usually, our journey starts with something that looks like this:
从根本上来说，我们可以将 CQRS 视为对软件架构命令查询分离规则的实现。在使用此方法的工作中，我注意到在最简单的 CQS 实现与真正成熟的 CQRS 之间有几个步骤。我想那些步骤可以顺利地引入我之前已经提到的规则。
虽然第一步没有实现我对 CQRS 的定义（但是有时候这么称呼的），但是他们还可以为你的软件引入一些真正的价值。每个步骤都引入一些有趣的想法，可以有助于构建或清理你的代码库/架构。
This is typical N-Layer architecture as all of us probably know. If we want to add some CQS here, we can “simply” separate business logic into Commands and Queries:
If you are working with legacy codebase this is probably the hardest step as separating side-effects from reads in spaghetti code is not easy. In the same time this step is probably the most beneficial one; it gives you an overview where your side effects are performed.
Hold for a second! You are talking about CQS, CQRS, but you have not defined what Command or Query is!
That is true. Let’s define them now! I’ll give you my personal, intuitive definition of Command and Query here. It is not exhaustive and definitely should be deepened before implementation.
Command – First of all,firing command is the only way to change the state of our system. Commands are responsible for introducing all changes to the system. If there was no command, the state of the system remains unchanged! Command should not return any value. I implement it as a pair of classes: Command and CommandHandler. Command is just a plain object that is used by CommandHandler as input value (parameters) for some operation it represents. In my vision command is simply invoking particular operations in Domain Model (not necessarily one operation per command).
Query – Analogically, query is a READ operation. It reads the state of the system, filters, aggregates and transforms data to deliver it in the most useful format. It can be executed multiple times and will not affect the state of the system. I used to implement them as one class with some Execute (…) method, but now I think that separation to Query and QueryHandler/QueryExecutor may be useful.
命令——首先，触发命令是唯一改变系统状态的方法。命令负责引起所有的对系统的改变。如果没有命令，系统状态保持不变！命令不应该返回任何值。我使用两个类来实现它：Command 和 CommandHandler 。Command 只是一个普通的对象，CommandHandler 将它用于表示某些操作的输入值（参数）。我认为命令是简单地调用领域模型中的特定操作（不一定是每个命令都有的操作）。
查询——同样的，查询是一个读操作。它读取系统的状态，过滤，聚总，以及转换数据，并将其转化为最有用的格式。它可以执行多次，而且不会影响系统的状态。我之前是使用一个有一些 Execute(…) 函数的类来实现它，但是现在我认为分离成 Query 和 QueryHandler/QueryExecutor 可能会更有用。
Going back to the diagram, I need to clarify one more thing; I’ve smuggled here one additional change, Model became the Domain Model. By the Model I understand a group of containers for data while Domain Model encapsulates essential complexity of business rules. This change is not directly affecting our further considerations as we are interested in architectural stuff here. But it is worth mentioning that despite the fact that the command is responsible for changing state of our system, the essential complexity should be placed in the Domain Model.
OK, now we can add new command or write new query. After short period of time it will be quite obvious that Domain Model that works well for writing is not necessarily perfect for reading. It is not a huge discovery that it would be easier to read data from some specialized model:
We can introduce separated model, mapped by ORM and use it to build our queries, but in some scenarios, especially when ORM introduces overhead, it would be useful to simplify this architecture:
I think that this particular change should be well thought out!
Now the problem is that we still have READ and WRITE model separated only at the logical level as both of them shares common database. That means we have separated READ model but most likely it is virtualized by some DB Views, in better case Materialized Views. This solution is OK if our system is not suffering from performance issues and we remember to update our queries along with WRITE model changes.
The next step is to introduce fully separated data models:
From my point of view this is the first model that fulfills original idea presented by Greg Young, now we can call it CQRS. But there is also a catch! I’ll write about it later.
Event Sourcing is an idea that was presented along with CQRS, and is often identified as a part of CQRS. The idea of ES is simple: our domain is producing events that represent every change made in system. If we take every event from the beginning of the system and replay them on initial state, we will get to the current state of the system. It works similarly to transactions on our bank accounts; we can start with empty account, replay every single transaction and (hopefully) get the current balance. So, if we have stored all events, we can always get the current state of the system.
While ES is a great method to store the state of the system is not necessarily required in CQRS. For CQRS, it is not important how Domain Model is actually stored and this is just one of options.
The idea of separated models seems to be quite clear and straight-forward when we are reading about CQRS but it seems to be unclear during implementation. What is the responsibility of WRITE model? Should I put all data into my READ model? Well, it depends!
I like to think about my WRITE model as about the heart of the system. This is my domain model, it makes business decisions, it is important. The fact that it makes business decisions is crucial here, because it defines major responsibility of this model: it represents the true state of the system, the state that can be used to make valuable decisions. This model is the only source of truth.
If you want to know more about designing domain models I recommend reading about Domain Driven Design technique philosophy.
当我们阅读 CQRS 时，分离模型的概念似乎非常清晰和直接，但在实现过程中似乎并不清楚。写模型的责任是什么？我是否应该将所有数据放入我的读取模型中？嗯，这得看情况！
In my first attempt to CQRS I have used WRITE model to build queries and … it was OK (or at least worked). After some time we reach the point in the project where some of our queries were taking a lot of time. Why? Because we are programmers and optimization is our second nature. We designed our model to be normalized, so our READ side were suffering from JOINs. We were forced to pre calculate some data for reports to keep it fast. That was an quite interesting, because in fact we have introduced a cache. And from my point of view this is the best definition of READ model: it is a legitimate cache. Cache that is there by design, and is not introduced because we have to release project and non-functional requirements are not met.
The label READ model can suggest that it is stored in a single database and that’s it. In fact READ model can be very complex, you can use graph database to store social connections and RDBMS to store financial data. This is the place where polyglot persistence is natural.
在我第一次尝试 CQRS 时，我使用了 WRITE 模型来构建查询……它是 OK 的(或者至少是有效的)。过了一段时间，我们到达了项目中需要花费大量时间进行查询的地方。为什么?因为我们是程序员，优化是我们的第二天性。我们将模型设计为规范化，因此我们的读取端受到连接的影响。我们被迫预先计算一些报告的数据以保持快速。这很有趣，因为实际上我们引入了缓存。在我看来，这是读取模型的最佳定义:它是一个合法的缓存。由于我们必须发布项目，而非功能性的需求没有得到满足，因此，缓存是通过设计来实现的。
标签读取模型可以建议它存储在一个数据库中，仅此而已。实际上读取模型可能非常复杂，你可以使用图形数据库来存储社会连接，使用 RDBMS 来存储财务数据。这是一个多语言持久性很自然的地方。