Copyright
이 모든 내용은 Pluralsight에 Dino Esposito가 올린 'Modern Software Architecture: Domain Models, CQRS, and Event Sourcing'라는 강의의 다섯번째 챕터를 듣고 정리한 것입니다(https://app.pluralsight.com/library/courses/modern-software-architecture-domain-models-cqrs-event-sourcing/table-of-contents). 강의 원작자분께 게시허가도 받았습니다.
Content
Discovering the Domain Architecture through DDD
The "Domain Model" Supporting Architecture
The CQRS Supporting Architecture
Designing Software Driven by the Domain
Outline
CQRS at a Glance
CQRS Regular
CQRS Premium
CQRS Deluxe
CQRS at a Glance
A single, all-encompassing object model to cover just any functional and non-functional aspects of a software system is a wonderful utopia, and sometimes it's even close to being an optical illusion
In module four, I briefly presented two ways to design a class for a sport match. One that incorporates business rules for the internal state and exposed behavior, and one that was a mere data transfer object trivial to persist, but devoid of business logic and with a public and filter the read write interface. In a single, all-encompassing object model perspective, the former class, the one with the reach behavior with methods like Start, Finish, and Goal, is just perfect.
But, are you sure that the same class would work well in a plain query scenario? When your user interface just requires to show the current score of a given match and that's it, is such a behavior rich class appropriate? Obviously, another simpler match class would be desirable to have, especially if it acts as a plain data transfer object.
So the question is, should you have both or should you manage to disable ethics of methods in query scenarios? In a single domain model scenario, none of the classes shown now are perfect, even though both can be adapted and used in a system that just does what it's supposed to do.
The behavior rich class is great in use cases in which the state of the system is being altered. The other is an excellent choice instead when the state of the system is being reported. The behavior rich class requires fixes to fully support persistence via an O/RM, and if used in a reading scenario, it would expose a behavior to the presentation layer, which could be risky. The other class has no business rules implemented inside, and because of its public getters and setters, is at risk of generating inconsistent instances.
CQRS (Command and Query Responsibility Segregation)
Defining a single class for commands and queries may be challenging, and you realistically can get it only at the cost of compromises, but compromises may be acceptable. Treating commands and queries differently through different stacks, however, is probably a better idea.
A command is then defined as an action that alters the state of the system and doesn't return data.
A query instead is an action that reads and returns data and doesn't alter the state of the system.
All that CQRS is all about is then implementing the final system so that both the responsibilities are distinct and have each its own implementation.
In terms of layers and system architecture, with CQRS we move from a classic multilayer architecture with presentation, application, domain, and infrastructure to a slightly different layout. In the new layout, presentation and infrastructure are unchanged. The application is crucial to have in the command stack, but can be considered optional in the query stack.
The huge difference is the implementation of the domain layer. In a CQRS, you may need to design a domain model only for the command stack, and can't rely on plain DTOs(Data Transfer Object : data container for moving data between layers. DTO is only used to pass data and does not contain any business logic. They only have simple setters and getters) and the direct data access in the query stack.
Aspects of CQRS
Why is CQRS then good for architects to consider?
Benefits
Separation of stacks allow for parallel and independent development, which means reuse of skills and people, and freedom to choose the most appropriate technologies without constraints(Distinct optimization). In addition, separation of stacks enables distinct optimization of the same stacks and lays the ground for scalability potential. The overall design of the software becomes simpler and the refactoring and enhancements come easier.
Flavors of CQRS
Quite simply, CQRS is a concrete implementation pattern, and it is probably the most appropriate for nearly any type of software application today regardless the expected lifespan and complexity. A point that often goes unnoticed is that there is not just one way of doing CQRS. I would recognize at least three different flavors of CQRS. Regular, Premium, and Deluxe.
CQRS Regular
CQRS is not just for overly complex applications, even though it's just there that it shines in concurrent collaborative, high traffic, high scale systems. The principle behind CQRS works well even with plain old CRUD applications.
CQRS for Plain CRUD Applications
So to make an existing CRUD system CQRS aware, instead of a monolithic block that takes care of database access for reading and writing, you have two distinct blocks. One for commands and database writes, and one for queries and database reads. If you are using Entity Framework or another object relational mapper framework to perform data access, all you do is just duplicate the context object through which you go down to the database. So you have one class library and model for commands, and one for queries. Having distinct class libraries is only the first step.
In the command stack, you just use the pattern that represents the best fit.
In the read stack, you just use the pattern that represents the best fit(O/RM of choice, ADO.NET, ODBC, Micro Frameworks, even stored procedures; whatever can bring data back the way you want). LINQ can help in a sense that it can easily bring iQueryable objects right in the presentation layer for direct data binding. Know that an IQueryable object describes a database query, but won't execute it until you call ToList or another analogous method. So this means that you can have high Queryable objects, carry it from the bottom of the system up to presentation, and you can result the query right to view model classes as expected by the front-end.
A nice tip in this context is using in the read stack of a CQRS solution, a read-only wrapper for the Entity Framework DbContext. In this way, when a query is performed, the presentation and application layers only have IQueryable data and write actions like those you can perform through save changes cannot just be performed.
Demo : Read-only Database Facade
public class Database : IDisposable
{
private readonly QueryDbContext _db = new QueryDbContext();
public IQueryable<Customer> Customers { get => _db.Customers; }
public void Dispose() { _db.Dispose(); }
}
You can use a simple database class instead of the native Entity Framework DbContext, but wrap has a private member of this new database class, the same DbContext object as you get it from Entity Framework. Next, all you do is return generic DbSet objects as high queryable, rather than as plain DbSet of T. That's it. It is a simple, but effective trick to use.
CQRS Regular in Action (SKIP)
CQRS Premium
All applications are CRUD applications to some extent, but at the same time, not all CRUD applications are the same. In some cases, it may happen that the natural format of data processed and generated by commands, so the data that captures the current state of the system, is significantly different from the ideal way of presenting the same data to users. This is an old problem of software design, and we developers solved it for decades by using adapters.
In CQRS, just having two distinct databases sounds like an obvious thing to do, if data manipulation and visualization needs would require different formats. The issue becomes how to keep the two databases in sync.
The issue is having distinct data stores for commands and queries makes development simpler and optimizes both operations, but at the same time, it raises the problem of keeping the two stores in sync. For the time in which data is not synced up, your app serves stale data, and the question is, is this acceptable?
The dynamics of a CQRS premium solution when two distinct data stores are used
CQRS Premium in Action
The user interface places a request and the application layer sends the request for action down to the command stack. To see the value of a premium approach, imagine that the application is essentially an even based application, for example, a scoring app in which the user clicks buttons whenever some event is observed and leaves the business logic the burden of figuring out what to do with the notified event. The command stack may do some processing and then saves just what has happened. The data store ends up being then a sort of log of actions triggered by the user. This form of data store makes also so easy to implement undo functionality. You just delete the last recorded action from the log, and that's it. The data store, the log of actions, can be relational or it can also be a NoSQL document database. It's basically up to you and your skills and preferences. Synchronization where _____ happens within or outside the current business transaction consists of reading the list of recorded action for a given aggregate entity, for example, in this case, the match, and extracting just the information you want to expose to the UI listeners. For example, a live scoring application that just wants to know about the score of a match and doesn't care about who scored the goals and when. At this point, the read data store is essentially one or more snapshot databases for clients to consume as quickly and easily as possible.
Message-based Business Logic
When you think about a system with distinct command and query stacks, inevitably the vision of the system becomes a lot more task-oriented. Tasks are essentially workflows and workflows are a concatenated set of commands and events.
A message-based architecture is beneficial as it greatly simplified the management of complex, intricate, and frequently changing business workflows. However, such a message-based architecture would be nearly impossible to achieve outside the context of CQRS that keeps command and query stacks neatly separated.
So, what is the point of messages? Abstractly speaking, a message can either be a command or an event.
public class Message
{
public DateTime TimeStamp { get; set; }
public String SagaId { get; protected set; }
}
So in code, you usually start by defining a base Message class that defines a unique ID for the workflow, and possibly a time-stamp to denote the time at which the message was received.
public class Command : Message
{
public String Name { get; protected set; }
}
public class Event : Message
{
// Any properties that may help retrieving and persisting events
}
Next, you derive from this base Message class additional classes for denoting a command, which is a message with a name in the typical implementation, or an event which is essentially just the notification of something that has happened, and you can add to further derive event classes properties that may help in retrieving information associated with that fact.
An event carries data and notifies of something that has happened. A command is an action performed against the back-end that the user or some other system components requested. Events and commands follow rather standard naming conventions. A command is imperative and has a name like submit order command; an event instead denotes a thing of the past and is named like order created.
In a message-based architecture, you render any business task as a workflow, except that instead of using an ad-hoc framework to define the workflow or plain code, you determine the progress of the workflow by sending messages.
The application layer sends a message and the command layer processes the message in much the same way that Windows, in early Windows OS. When a message, whether a command or an event is received, the command stack originates a task. The task can be a long-running state for process, as well as a single action or stateless process. A common name for such a task is saga. Commands usually don't return data back to the application layer, except perhaps for some quick form of feedback, such as whether the operation completed successfully, was refused by the system, or the reason why it failed. The application layer can trigger commands following user actions, incoming data from asynchronous streams, or other events generated by previous commands.
For a message-based system to work, some new infrastructure is required, the bus and an associated set of listeners are the main building blocks.
The core element of a message-based architecture is the workflow. The workflow is the direct descendent of user defined flowcharts. Abstracted to a saga instance, the workflow advances through messages, commands, and events. The central role played by workflows and flowcharts is the secret of such an architecture, as simple that can be easily understood even by domain experts, because it resembles flowcharts, and it can also be understood by developers, because it is task-oriented and then so close to the real business and so easy to mirror with software.
CQRS Deluxe
CQRS Deluxe is a flavor of command query separation that relies on a message-based implementation of the business tasks. The read stack is not really different from other CQRS scenarios we have considered so far, but a command stack takes a significantly different layout, a new way of doing old things, but a new way that is hopefully a lot more extensible and resilient to changes.
In this CQRS design, the application layer doesn't call out any full-fledged implementation of some workflows, but it simply turns any input it receives into a command and pushes that to a new element, the bus.
The bus is generically referring to a shared communication channel that facilitates communication between software modules. The bus here is just a shared channel, and doesn't have to be necessarily a commercial product or an open source framework. It can also and simply be your own class.
At startup, the bus is configured with a collection of listeners, that is, components that just know what to do with incoming messages. There are two types of message handlers called sagas and handlers. A Saga is an instance of a process that is optionally stateful, maintain access to the bus, is persistable, and sometimes long-running. A handler instead is a simpler, one executer of any code bound to a given message. The flowchart behind the business task is never laid out entirely. It is rather implemented as a sequence of small steps, each calling out the next or raising an event to indicate that it is done. As a result, once a message is pushed to the bus, the resulting sequence of actions is partially predictable and may be altered at any time adding and removing listeners to and from the bus. Handlers end immediately, whereas sagas, which are potentially long-running, will end at some point in the future when the final message is received that ends the task that saga represents. Sagas and handlers interact with whatever family of components exist in the command stack to expose business logic algorithms.
Most likely, even though not necessarily, you'll have a domain layer here with a domain model and domain services are calling to the architecture we discussed in module four. The domain services, specifically repositories, will then interact with the data store to save the state of the system.
The use of the bus also enables another scenario, event sourcing. Event sourcing is a technique that turns detected and recorded business events into a true part of the data source of the application. When the bus receives a command, it just dispatches the message to any registered listeners, whether sagas or handlers. But when the bus receives an event from the presentation or from other sagas, it may first optionally persist the event to the event store, a log database, and then dispatch it to listeners. It should be noted that what I describe here is the typical behavior one expects from a bus when it comes to orchestrating the steps of a business task. As mentioned, CQRS Deluxe is particular just because of the innovative architecture of the command stack. The read stack instead just uses any good query code that does the job. Therefore, it means your O/RM of choice, possibly LINQ, and ad-hoc storage, mostly relational. And the issue of stale data and synchronization is still here, and in the context of a CQRS Deluxe solution, the code that updates synchronously or asynchronously, the read database can easily take the form of a handler.
CQRS Deluxe implementation
INSIDE THE BUS
The bus, in particular, is a class that maintains internally a list of known saga types, a list of running saga instances, and the list of known handlers. The bus gets messages and all it does is dispatching messages to sagas and handlers. Each saga and handler, in fact, will declare which messages they're interested in, and in this regard, the overall work of the bus is fairly simple.
A saga is characterized by two core aspects.
The command or event that starts the process
The list of commands and events that saga can handle
The resulting implementation of a saga class is then not rocket science.
public class CheckoutSaga : Saga<CheckoutSagaData>,
IStartWith<StartCheckoutCommand>,
ICanHandle<CancelCheckoutCommand>,
ICanHandle<PaymentCompletedEvent>,
ICanHandle<PaymentDeniedEvent>,
ICanHandle<DeliveryRequestRefusedEvent>,
ICanHandle<DeliveryRequestApprovedEvent>
{
public void Handle(StartCheckoutCommand message) { ... }
...
}
The class declares messages it is interested in through multiple interfaces, and then its body is full of handle methods, one for each type of supported message.
More about Sagas
Sagas must be identified by a unique ID
Each saga must be uniquely identified by an ID. The ID can be a number of things, it can be a GUID, or more likely it is the ID of the aggregate the saga is all about. In general, a saga is a process that involves some collection of entities relevant in the business context. Any combination of values that uniquely identifies(in the context) the main act or in the process is any way a valid identifier for the saga.
Sagas might be persistent and stateful
A saga might be stateful and needing persistence. In this case,
Persistence is care of the bus
State of the associated aggregate must be persisted
Sagas might be stateless
a saga might be in some cases stateless as well. In the end, a saga is what you need it to be. If you need it to be stateless, then the saga is a mere executive of orders brought by commands or it just reacts to events.
Extending a Solution
The point behind CQRS Deluxe and sagas in the end is that it makes far easier to extend an existing solution when new business needs and new requests come up. For this extra level of flexibility, you pay the cost of having to implement a bus and a more sophisticated infrastructure for the business logic. This is exactly what I have called so far the message-based approach.
So, let's say you've got a new handling scenario for an existing event or you just got a new request for an additional feature. In this case, all you do is write a new saga or a new handler and then just register it with the bus. That's it. More importantly, you don't need to touch the existing workflows and the existing code as the pieces of the workflow are, for the most part, independent from one another.
More About the Bus
You can surely write your own bus class. Whether it is a good choice depends on the real traffic hitting the application, the optimizations, the features, and even the skills of involved developers. For sure, for example, you might need at some point to plug into the bus some queuing and/or persistence agents.
An alternative to writing your own bus class, you can look into existing products and frameworks.
NServiceBus from Particular Software
Rebus from Rebus-org
MassTransit from Pandora
CQRS Deluxe Code Inspection (SKIP)
출처
이 모든 내용은 Pluralsight에 Dino Esposito가 올린 'Modern Software Architecture: Domain Models, CQRS, and Event Sourcing'라는 강의의 다섯번째 챕터를 듣고 정리한 것입니다(https://app.pluralsight.com/library/courses/modern-software-architecture-domain-models-cqrs-event-sourcing/table-of-contents). 제가 정리한 것보다 더 많은 내용과 Demo를 포함하고 있으며 최종 Summary는 생략하겠습니다. Microsoft 지원을 통해 한달간 무료로 Pluralsight의 강의를 들으실 수도 있습니다.
'Programming > Architecture' 카테고리의 다른 글
(Modern Software Architecture) Designing Software Driven by the Domain (0) | 2018.02.26 |
---|---|
(Modern Software Architecture) Event Sourcing (0) | 2018.02.25 |
(Modern Software Architecture) The "Domain Model" Supporting Architecture (0) | 2018.02.22 |
(Modern Software Architecture) The DDD Layered Architecture (0) | 2018.02.14 |
(Modern Software Architecture) Domain-Driven Design (DDD) (0) | 2018.02.13 |