
Matthias Noback About Ideal Architecture - Layers, Ports, and Adapters (Part 2 - Layers)
- Transfer
In 2017, Matthias Noback (author of A year with Symfony ) published a three-article series in which he described his views on the ideal corporate application architecture that has emerged over many years of practice. The first part is introductory and not of particular interest (you can read in the original ) . The second translation is this article. A third translation will be available shortly.
For me, one of the mandatory requirements for a "clean" architecture is the competent separation of the application code into layers . The layer itself does nothing, all the salt is in how it is used and what restrictions are imposed on the components that belong to it. Let's philosophize a bit before considering interesting practical techniques.
Why do we need layers
- Layers help to hide / protect what is under them. You can perceive a layer as a filtering barrier: the data transmitted through it must be validated before moving on to the next one. They should be reduced to a format that allows other layers to work correctly with them. A layer also determines which data and functions from a deeper layer can be used externally.
- Layers clearly delineate responsibilities, and therefore the location of classes in your code. If you achieve strict agreements within your team about which layers are used in your application and what each of them is responsible for, then it will always be easy for you to find the class you need or decide where to add a new one, just knowing its purpose.
- Through the use of layers, you can freely change the priority and order of the stages of application development. You can develop a project sequentially, starting from the core of business logic, laying layer upon layer on it. And you can invert the process and start by developing a user interaction layer. This item is quite important for us, because thanks to it you can develop most of the application before deciding on the ORM, database, framework, etc.
- A large number of old software contains code that is not divided into layers, which can be called "spaghetti" code: you can call and use whatever you want, any methods and structures in any part of the project. Using a layer system (in the right way) you can achieve a high level of separation of concerns . If you document these rules and monitor their compliance with the review code, then you will greatly reduce the speed of rolling your project down to rank
gavnokoda"technical debt" You, of course, write tests. The competent designed layer system, incredibly simplifies testing. Different types of tests are suitable for code from different layers. The purpose of each test becomes more apparent. The test suite as a whole becomes more stable and faster.
However, we do have an alarmist from Twitter: the
OOP version of the spaghetti code is a climbing code, with an overabundance of layers.
Personally, I never met code lasagna, but I saw a lot of noodle code. True, it happened that I wrote code in which I made serious architectural errors and incorrectly divided the application into layers, which brought some problems. In this article, I describe what I think is the best set of layers, most of which are described in Vaughn Vernon's book "Implementing Domain-Driven Design" (link below). Please note that the layers do not have a rigid binding to DDD, although they make it possible to create clean domain models, if the developer so desires.
Directory and namespace structure
Inside src/
I have directories for each context ( Bounded Context ), for example, which I select in my application. Each of them also serves as the root namespace for its classes.
Inside each context, I create directories for each of the layers:
- Domain
- Application
- Infrastructure
src/
{BoundedContext}/
Domain/
Model/
Application/
Infrastructure/
I will briefly describe each of them.
Layer 1 - Domain (model / core)
The domain layer contains classes for known DDD types / patterns:
- Entities
- Value objects
- Domain events
- Repositories
- Domain services
- Factories
- ...
Inside the Domain folder, I create a subfolder Model, inside it - the directories for each of the aggregates (Aggregate root). The aggregate folder contains all the pieces associated with it (value objects, domain events, repository interfaces, etc.)
Please note that the code from the domain layer does not come into contact with the real world. And if not for the tests, then no one could access his objects directly (this is done through the upper layers). Tests for the domain model must be extremely modular. Since the domain layer does not directly interact with the file system, network, database, etc., we get stable, independent, clean and fast tests.
Layer 2 - (wrapper for domain): Application layer
The application layer (Application Layer) contains the classes of commands and their handlers . The command is an indication of something that needs to be executed. This is a regular DTO (Data Transfer Object) containing only primitive values. There should always be a command handler that knows how to execute a particular command. Usually the command handler (also called application service ) is responsible for all the necessary interactions - it uses the data from the command to create (or retrieve from the database) the aggregate, performs some operations on it, and can save the aggregate after that.
The code of this layer can also be covered with unit tests, however, at this stage, you can begin to write and acceptance. Here is a good article on this topic Modeling by Example from Konstantin Kudryashov.
Layer 3 (wrapper for application) - Infrastructure
The code written in the previous layer is also not called by anyone other than tests. And only after adding the infrastructure layer, the application becomes fully usable.
The infrastructure layer contains the code necessary for the application to interact with the real world - users and external services. For example, a layer may contain code for:
- HTTP works
- Communication with the database
- Email sending
- Pushing
- Getting time
- Random number generation
- Etc
The code for this layer should be covered by integration tests (in Freeman and Pryce terminology ). Here you test everything for real - a real base, a real vendor code, real external services. This allows you to verify the performance of those things that are not under your control but are used in your application.
Frameworks and libraries
All frameworks and libraries interacting with the outside world (file system, network or database) should be called in the infrastructure layer. Of course, the domain and application layer code often needs the functionality of ORM, HTTP client, etc. But he should use it only through more abstract dependencies. As required by the rule of dependencies .
Dependency rule
The dependency rule (formulated by Robert C. Martin in The Clean Architecture ) states that on each application layer you should depend only on the code of the current or deeper layer. This means that the domain code depends only on itself, the application layer code on its own code or domain, and the infrastructure layer code can depend on everything. Following this rule, it is impossible to make a dependency on a code from infrastructural in a domain layer.
But blindly following a rule, not understanding what its true meaning is, is a rather stupid undertaking. So why should you use the dependency rule? By following this rule, you guarantee that the clean code of the layers of the application and domain layers will not be tied to the "dirty", unstable and unpredictable infrastructure code. Also, applying the dependency rule, you can replace anything in the infrastructure layer without touching or changing the code of the deeper layers, which gives us rich opportunities for rotation and portability of components.
This method of reducing the connectedness of modules has long been known as the Dependency Inversion Principle - the letter "D" in the SOLID formulated by Robert Martin: "The code should depend on abstractions, not on implementations." The practical implementation in most OOP languages is to allocate a public interface for all the things you can depend on (the interface will be an abstraction) and create a class that implements this interface. This class will contain details that are not important for the interface, therefore this class will be the implementation that is mentioned in the inversion principle.
Architecture: Delaying Technological Solutions
Applying the proposed set of layers along with the dependency rule, you can get a lot of goodies when developing:
- You can experiment a lot before making such important decisions as, for example, the “DBMS used”. You can also safely use different databases for different cases as part of working with the same model.
- You can postpone the decision about the framework used. This will not allow you to become a "Symfony application" or a "Laravel project" at the very beginning of development.
- Frameworks and libraries will be placed at a safe distance from the model code. This will help a lot when updating major versions of these frameworks and libraries. It will also help minimize code changes and labor if you ever want to use, for example, Symfony 3 instead of Zend Framework 1.
All this looks extremely tempting: I like the ability to seamlessly replace application components + I like to make important architectural decisions not before the project starts (based on my past experience and guesses), but when the real cases of using different parts of the application begin to become clear, and I have the ability to choose the right solutions based on existing needs.
Conclusion
As mentioned earlier, this application bundle option gets along well with any framework, because its place is clearly defined in the infrastructure layer.
Some people think that in my version there are “too many layers”. I don’t understand how 3 layers can be considered too large, but if it bothers you then you can remove the applied one. You will lose the ability to write acceptance tests (they will become somewhat similar to system ones - slower and more fragile) and you will not be able to test the same functionality called for example from the web interface and console command without duplicating code. In any case, you will greatly improve the architecture of your project due to the separation of business logic and infrastructure.
It remains to consider in more detail the infrastructure layer. So we will smoothly move on to the topic of hexagonal architecture (ports and adapters). But all this, in the next part.
Further reading
- Growing Object-Oriented Software Guided by Tests by Steve Freeman and Nat Pryce
- Screaming Architecture by Robert C. Martin
- The Clean Architecture by Robert C. Martin
- Implementing Domain-Driven Design , chapter 4: "Architecture" and chapter 9: "Modules", by Vaughn Vernon
You can also check out Deptrac , a tool to help you follow the rules for using layers and dependencies.