Some people say ：“CQRS It is difficult to ！”
Is it? ？ ok , I used to think that ！ but , When I started using CQRS When I wrote my first software , It will soon break itself . more importantly , I think in the long run , It's easier to maintain software in this way .
I started thinking ： Why do people think it's so difficult and complicated in the beginning ？ I have a theory ： It contains rules ！ It's always uncomfortable to enter a world with rules , We need to adapt to these rules . In this article , I want to prove that in this case , These rules are very easy to understand .
On the way to CQRS On the way …
basically , We can CQRS It is regarded as the implementation of query separation rules for software architecture commands . In the work of using this method , I noticed in the simplest CQS Achieve and truly mature CQRS There are several steps in between . I think those steps can smoothly introduce the rules I have mentioned before .
Although the first step didn't come true, I was right CQRS The definition of （ But sometimes it's called ）, But they can also bring some real value to your software . Every step brings in some interesting ideas , Can help build or clean up your code base / framework .
Usually , Our journey begins here ：
We probably all know that , This is a typical N- Layer architecture . If we want to add something here CQS, We can “ simply ” Separate the business logic layer into command and query ：
If you're still using the old code base , This is probably the most difficult step , It's not as easy as separating side effects from reading spaghetti code . At the same time, this step is probably the most beneficial one ; It will give you an overview of where side effects are performed .
wait a minute ！ You're talking about CQS,CQRS , But you haven't defined what a command or query is yet ！
you 're right . Let's start defining them ！ ad locum , I'll give you my personal 、 Intuitive definition of commands and queries . It's not comprehensive , And it has to be deepened before it can be realized .
command —— First , The trigger command is the only way to change the state of the system . Commands are responsible for causing all changes to the system . If there is no order , The state of the system remains unchanged ！ The command should not return any value . I use two classes to implement it ：Command and CommandHandler .Command It's just an ordinary object ,CommandHandler Use it to represent input values for some operations （ Parameters ）. I think a command is simply a call to a specific operation in the domain model （ It doesn't have to be an operation for every command ）.
Inquire about —— alike , A query is a read operation . It reads the state of the system , Filter , Gather together , And converting data , And turn it into the most useful format . It can be executed multiple times , And it doesn't affect the state of the system . I used to use one with some Execute(…) Function to implement it , But now I think separation is Query and QueryHandler/QueryExecutor It might be more useful .
Back to the diagram , I need to clarify something ; I have secretly made a supplementary revision , Model to domain model . Because I think of a model as a set of data containers , The domain model includes the essential complexity of business rules . Because we're interested in the architecture here , This change will not directly affect our further consideration . But it's worth mentioning , Although commands are responsible for changing the state of the system , The essential complexity should be put in the domain model .
well , Now we can add new commands or write new queries . In a short time , Obviously , Domain models for writing are not necessarily suitable for reading . It's easier to read data from a particular model , This is not a major discovery ：
We can introduce a separation model , from ORM Map and build queries , But in some cases , Especially when ORM When introducing overhead , It will help simplify the structure .
I think this particular change should be well considered ！
The problem now is that we still have read and write that are separated only at the logical level DB Views are virtualized , It's better to materialize the view . If our system has no performance problems , And the model , Because they share a common database . This means that we have separated the read model , But it is most likely to be and we remember to update the query when the writing model changes , Then this plan is feasible .
The next step is to introduce a completely separate data model ：
in my opinion , This is the first one that fits Greg Young A model of the original idea put forward , Now we call it CQRS . But it still has problems ！ I'll write later .
CQRS != Event source
The origin of the event is related to CQRS A concept put forward together , Usually marked as CQRS Part of .
ES（Event Sourcing） The concept is simple ： Our domain generated events represent every change in the system . If we start recording every event from the system , And it starts to reappear from the beginning , We'll get the current state of the system . It's similar to a bank account transaction ; We can start with an empty account , Reproduce each individual transaction , then （ Hopefully ） Get the current balance . therefore , If we've stored all the events , We can get the current state of the system .
although ES It's a good way to determine the state of the storage system , however CQRS It doesn't have to be . about CQRS , How domain models are actually stored doesn't matter , And it's just an option .
Read model and write model
When we read CQRS when , The concept of separation model seems very clear and direct , But it doesn't seem clear in the implementation process . What's the responsibility of writing models ？ Should I put all the data into my read model ？ Um. , It depends ！
I like to see my writing model as the core of the system . This is my domain model , It makes business decisions , It is very important . The fact that it makes business decisions is crucial here , Because it defines the main responsibilities of the model : It represents the real state of the system , The state that can be used to make valuable decisions . This pattern is the only source of truth .
If you want to learn more about designing domain models , I recommend that you read the philosophy of Domain Driven Design Technology .
Read the model
In my first attempt CQRS when , I use the WRITE Model to build queries …… It is OK Of ( Or at least it works ). After a while , We got to the point in the project where we needed to spend a lot of time making queries . Why? ? Because we are programmers , Optimization is our second nature . We designed the model to be normalized , So our readers are affected by the connection . We were forced to pre compute some of the reported data to keep it fast . This is very interesting. , Because we actually introduced caching . in my opinion , This is the best definition of the read model : It's a legitimate cache . Because we have to release the project , The non functional requirements are not met , therefore , Caching is done by design .
The tag reading model can suggest that it be stored in a database , That's it . In fact, reading models can be very complex , You can use a graphics database to store social connections , Use RDBMS To store financial data . This is a natural place for multilingual persistence .
A well-designed read model is a series of trade-offs , For example, pure normalization and pure non normalization . If your project is small , And most reads can be done efficiently based on the write model , So creating a replica is a waste of time and computing power . however , If your writing model is stored as a series of events , So it would be very useful to use all the necessary data without replaying all the events from the beginning . This process is called fast read derivation , in my opinion , It is CQRS One of the most complex things in the world , This is one of the difficulties I mentioned earlier . As I said before , The read model is a form of caching , As we know ：
There are only two difficult things in Computer Science : Cache invalidation and naming . ——Phil Karlton
I said it was a “ legal ” The cache of , The word also has an extra meaning for me , In our system , We have a clear reason to update the cache . The events generated by our domain model are the natural reason for updating the read model .
If our models are physically separate , So synchronization will take some time , It's natural , But this time it's very scary for the business people . In my project , If every part works properly , that READ model Out of sync time is usually negligible . However , When developing more complex systems , We definitely need to think about time risk . Well designed UI It's also helpful to deal with the final consistency .
We must assume that , Even if the read model is updated synchronously with the write model , Users still make decisions based on stale data . Unfortunately , We're not sure if the data is still fresh when it's presented to users （ For example web The browser presents ).
How to integrate CQRS Introduce into the project ？
I Believe CQRS It's so simple , There is no need to introduce any framework . You can start with less than 100 The simplest implementation of line code begins , Then introduce new features to extend it when needed . You don't need any magic , because CQRS It's simple , And it simplifies the software . This is my realization ：
I define several interfaces to describe commands and their execution environment . Why do I use two interfaces to define a command ？ I do this because I want to keep parameters as normal objects , In this way, you can create... Without any dependencies . My orders handler It can be downloaded from DI Request dependency in container , And besides in the test , You don't need to instantiate it anywhere . in fact ,ICommand Interfaces act as tags here , To tell the developer if he can use this class as a command .
This definition is very similar IQuery Interface , But it also defines the types of query results . This is not the most elegant solution , But the result is to verify the type returned at compile time .
my CommandDispatcher Quite short , It is only responsible for instantiating the appropriate command helper for a given command and executing it . To avoid manually entering commands to register and instantiate , I have used DI Containers to do this , But if you don't want to use any DI Containers , You can still do it yourself . As I said , This implementation will be simple , I believe so . The only problem may be the noise introduced by generics , It can be frustrating at first . This implementation is really simple to use . Here's an example of a command and helper ：
Only need to SignOnCommand To the dispatcher to execute this command ：
this is it .QueryDispatcher It looks very similar , The only difference is that it returns some data , Thanks to the general code I wrote before ,Execute Method returns strongly typed results ：
Like I said , This implementation is extensible . for example , We can order for dispatcher Introducing transactions , Instead of creating decorator To change the original implementation .
By using this pseudo method , We can easily extend the command and query dispatcher . You can add “ Give up immediately ” Command execution method and a large number of logs .
As you can see ,CQRS It's not that hard , The basic idea is clear , But you need to follow some rules . I'm sure this article doesn't cover everything , That's why I suggest you read more .
CQRS file ,Greg Young Writing
Clarified CQRS,Udi Dahan Writing
CQRS, Martin Fowler Writing
CQS, Martin Fowler Writing
“ Realization DDD” Vaughn Vernon Writing