Developer Cookbook: DDD Recipes (Part 3, Application Architecture)
In previous articles, we highlighted the scope of the approach and examined the main methodological principles of Domain Driven Design .
In this article, I would like to identify the main modern approaches to building an enterprise system architecture: Supple, Screaming, Clean and give them their clear interpretation in the form of a complete ready-made solution.
In the following, we will consider each design pattern in detail: let’s define the scope, give examples of code, and highlight recommended practices. As a result, we will write a ready microservice.
In the last article, we dwelled on the fact that DDD includes the practice of implementation through the model. The subject area should be described through your code. Let's try to figure out how to do this.
In his book, Eric Evans provides a number of design patterns recommended for use, and designates this approach as flexible:
In the name of architecture flexibility, many unnecessary constructions were piled up in programs. Extra levels of abstraction and indirect references are more likely to interfere than help in this matter. Look at the architecture that really inspires programmers involved in its refinement, and you will see, as a rule, something very simple. But simple does not mean easy to perform. To create such elements that can be assembled into complex systems and it is not difficult to understand, it is necessary to combine “devotion” to design according to the model with a rather strict architecture style. A certain design skill is needed not only to create something, but even to use ready-made.
Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
The presented set of design patterns is not a strict architecture or a ready-made solution, but rather food for thought.
Similar thoughts occurred in the minds of many developers and designers of complex systems.
In 2011, there was an article by Robert Martin - Screaming Architecture , which says that your code does not just have to describe the subject area, but yell about it, preferably with obscenities.
So what is your application scream? The structure of the package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring / Hibernate, or ASP?
Robert C. Martin, September 30, 2011
Robert says that the code for your application should reflect the activity of the application, instead of adjusting to the rules of the framework. The framework structure should not limit your architecture. The application, in turn, should not be tied to the database or http protocol, these are just storage and delivery mechanisms. The bounding box is a tool. You should not become an adept frame maker. Tests of your application are tests of the logic of its operation, and not testing of the http protocol.
A year later, Robert Martin’s next article, The Clean Architecture . In it, the author tells how to make the code scream. After studying several architectures, he outlines the basic principles:
- Independence from the framework. The architecture does not depend on any existing library. This allows you to use frameworks as tools, not as constraints that bind your hands.
- Testability Business rules can be tested without a user interface, database, web server, or any other technical means.
- Independence from user interface. The user interface can be easily changed without changing the rest of the system. For example, a web interface can be replaced with a console interface without changing the business logic.
- Independence from the database. You can exchange Oracle or SQL Server for Mongo, BigTable, CouchDB or something else. The logic of your application should not be tied to a database.
- Independence from the impact of the environment. In fact, your business rules simply do not know anything about the outside world.
On Habré already published a very good article Delusions Clean Architecture . Its author, Jeevuz , chewed very well the subtleties of understanding this approach. I strongly recommend to get acquainted with it as well as with original materials.
The description of the approach presented above does not look so unambiguous. As part of the development of the architecture of a number of complex corporate systems, I and my colleagues developed a fairly clear interpretation of the described approaches, which I am going to present below.
Before the advent of computers and programming languages, paper workflow was used to build and manage systems with complex business logic. The result of any process was a document that ultimately described a particular business object. As a result, clerical work was reduced to three simple actions :
- Document creation
- Document processing
- Work with the archive of documents
- Document presentation
Document - fixing information about the economic activity of a particular real business object.
Please note that the document itself is not a real business object, but only its Model . At the moment, paper documents are being superseded by electronic ones. A document can be a record in a table, a picture, a file, a sent letter, or any other piece of information.
I would not want to continue to use a Word document, since it will make more confusion, we will use the concept of essence (Entity) from DDD terminology. But you can imagine that now your entire system is an electronic document management system that performs four simple Actions .
Action (Action) - the structural unit of the business model; a relatively complete separate act of a perceived goal, arbitrariness and premeditation of the individual activity of a business object, distinguished by the end user.
A good example of Dacevtia is theatrical act. Theater simulates events from real life. The act is a meaningful part of the play. But in order to make the story complete, you need to lose several acts in a strictly defined order. Such an order in our architecture we will call Mode .
Mode (Conduction) - a set of Actions in a certain order, which has a complete sense, bringing benefit to the end user.
For such modes of operation, a selective jig or selector was invented . More specifically, the " US2870278A patent was obtained for the plurality of sequences of operations" . We know this device as a "twist" of a washing machine. Architectural "twist" is given at the beginning of the article.
The variability of the approach is manifested in the fact that with such an architecture you can choose any of the four Modes , passing which you will not perform unnecessary Actions .
Starting the washing machine, you can select the mode: wash, rinse or spin. If you choose to wash, your machine will still rinse the laundry, and then it will wring out. With rinsing included, you are sure to get a spin. Spin - the final action in the process of washing it is the most "simple". In our architecture, the simplest Activity - Representation , and begin with it.
If we talk about a clean view without accessing the database or an external source, then we give out some static information: html-page, file, directory lying in the form of json'a. We can even issue just a Code response - 200:
Let's write the simplest "Health checker"
moduleHealthclassEndpoints < Sinatra::Base
get '/check'do; endendend
In the most primitive form, our scheme will look like this:
Я прошу заметить, что во фреймворке Sinatra класс Endpoints объединяет в себе как Router, так и Controller в одном классе. Не нарушает ли это принципа единственной ответственности? По факту, Endpoints это не класс, а слой, выраженный через класс, и зона его ответственности на более высоком уровне.
Ок, а как же Router и Controller? Они представлены не набором классов, а наименованием и реализацией функции. А статический файл это вообще файл. Один класс отвечает одной ответственности, но не пытайтесь выразить каждую ответственность через класс. Исходите из практичности, а не из догматизма.
Work with the storage system (Storage)
Business is demanding on the availability of your application. Why would anyone need your service if we cannot use it at the right moment? To ensure data integrity, we record a change in the state of a business object after each processing.
To retrieve an object from storage, no business logic is required. Imagine that we are automating the activities of a hotel chain and we have a magazine of guests at the front desk. We decided to look at the information about the visitor.
moduleReceptionclassEndpoints < Sinatra::Base# Show item
get '/residents/:id', provides::jsondo
resident = Repository::Residents.find params[:id]
Working with the storage system in the form of a graphic scheme:
As we can see, the communication between the level responsible for storage and the level responsible for data presentation is implemented through the Response model. This model does not belong to any of these layers. In fact, this is a business object and it is located on the layer responsible for the business logic.
If it comes to the fact that the object model changes based on its properties without adding new data, then we can directly access the Interactor layer . Layer interactor is the key to our application, that it describes all the business logic in the form of individual variants use (Use Cases) and that it is the change in Essences .
Consider this use case. In our hotel, the visitor is already registered, but we celebrate each his arrival or departure.
moduleReceptionclassEndpoints < Sinatra::Base# Register resident arrival
post '/residents/:uid/arrival', provides::jsondo
result = Interactors::Arrival.call(resident_id: params[:id])
endend# Register resident departure
post '/residents/:uid/departure', provides::jsondo
result = Interactors::Departure.call(resident_id: params[:id])
Let's stop a little. Why not to make implementation by one method with parameter
Departureradically different. If a guest has come to us, then we need to check whether the cleaning has ended, whether there have been any new messages for him, etc. When he leaves, we, on the contrary, must initiate cleaning if necessary. In turn, we don’t even remember the messages, because if he were in a hotel, we would call him right away. It is all this business logic that we prescribe on the Interactor layer .
But what should we do if we have data from the outside? This is where the Data Collect action connects .
During the first registration of a guest at a hotel, he fills out a registration form. This form is verified. If the data is correct, then a business process is registered. The process returns data - the created “Resident” business model. We present this model to the resident in a readable form:
moduleReceptionclassEndpoints < Sinatra::Base# Register new resident
post '/residents', provides: [:json] do
form = Forms::Registration.new(params)
complete! form do
check! form.result do
Schematically it looks like this:
Rules of the game (Rules)
- Variable system in terms of processes is divided into Actions .
- The sequence of Actions is determined by the Mode .
- Modes are incremental.
- The more "complex" mode complements the more "simple" one for strictly one action.
- Each action takes place within a single Layer .
- Each layer is represented by a Class .
- Inside a layer there can be Classes-Layers and Classes-Responsibilities .
- Communication takes place only between the Layer and the Inner Layer Class .
- View Models are exceptions.
- Error handling should occur at the Class-Layer level .
This approach has a high threshold of entry. Its application requires a great experience from the designer for a clear understanding of the tasks to be solved. Difficulty is also a variety of choice of the necessary tool. But, despite the complexity of the structure, implementation at the code level is incredibly simple and expressive. Although it contains a number of conventions and powers of attorney. In the future, we analyze each design pattern separately, describe how to create it, test it, and designate the scope. And in order not to get confused in their diversity, the full map is offered:
- Проблемно-ориентированное проектирование, Эрик Дж. Эванс
- Кейт Матсудейра: Масштабируемая веб-архитектура и распределенные системы