From the translator: this article was written by Uncle Bob in August 2012, but, in my opinion, quite relevant to this day.
Over the past few years, we have seen a variety of ideas regarding system architecture. Each of them gave the output:
')
- Independence from the framework . The architecture does not depend on the existence of any library. This allows the framework to be used as a tool, rather than squeezing its system into the framework of its limitations.
- Testability Business rules can be tested without a user interface, database, web server, or any other external component.
- Independence from UI . The user interface can be easily changed without changing the rest of the system. For example, the web interface can be replaced by the console, without changing business rules.
- Independence from the database . You can change Oracle or SQL Server to MongoDB, BigTable, CouchDB or something else. Your business rules are not associated with the database.
- Independence from any external service . In fact, your business rules simply do not know anything about the outside world.
The diagram at the beginning of this article is an attempt to combine all these ideas into a single effective scheme.
Dependency Rule
The concentric circles in the diagram represent the different layers of software. In general, the further you go, the more general the level becomes. Outer circles - mechanics. Internal circles are politics.
The main rule that makes this architecture work is the
Rule of Dependencies . This rule states that dependencies in the source code can only indicate inside. Nothing from the inner circle can know anything about the outer circle, nothing from the inner circle can point to the outer circle. This applies to functions, classes, variables, etc.
Moreover, the data structures used in the outer circle should not be used in the inner circle, especially if these structures are generated by the framework in the outer circle. We do not use anything from the outer circle to affect the inner.
Entities
Entities are determined by the business rules of the enterprise. An entity can be an object with methods or it can be a set of data structures and functions. It does not matter how long the entity can be used in different applications.
If you are writing just a single application, in this case the entities are the business objects of this application. They encapsulate the most common high-level rules. It is least likely that they will change with any external changes. For example, they should not be affected when changing page navigation or security rules. External changes should not affect the entity layer.
Scenarios
This layer implements the specifics of business rules. It encapsulates and implements all uses of the system. These scripts implement the flow of data to and from the
Entities layer to implement business rules.
We do not expect changes in this layer that affect
Entities . We also do not expect that this layer may be affected by external changes, such as databases, user interfaces, or frameworks. This layer is isolated from such problems.
We, however, expect changes in the application to affect
Scripting . If there are any changes in the behavior of the application, they will undoubtedly affect the code in this layer.
Interface Adapters
The software in this layer is a set of adapters that convert data from the format that is most convenient for
Scenarios and
Entities into the format that is most convenient for further use, for example, in a database. It is this layer, for example, that will fully contain the MVC architecture.
Models are most likely data structures that are passed from controllers to
Scenarios , and then back from
Scenarios to
Views .
In the same way, the data is converted, in this layer, from the form most convenient for
Scenarios and
Entities , to the form most convenient for permanent storage, for example in a database. The code inside this circle should not know anything about the database. If the database is a SQL database, then any SQL statements should not be used at this level.
Framework and drivers.
The outer layer usually consists of frameworks, databases, UI, etc. As a rule, a lot of code is not written in this layer, except for the code that communicates with the inner circles.
This is a cluster of parts. The Internet is a part, the DB is a detail, we keep these things outside to reduce their influence.
Only four laps?
No, the circles are sketchy. You may decide that you need more than these four. There is no rule stating that you should always have only these four. However, the
Dependency Rule always applies. Dependencies in the source code always point inside. As you move inside, the level of abstraction increases. The outer circle is the level of detail. The inner circle is the most common.
Crossing borders.
The lower right side of the diagram shows an example of how we cross the boundaries of a circle.
Controllers and
Views interact with
Scenarios from the next layer. Take a snapshot on the control flow. It starts in the
Controller , moves through the
Scenario , and then ends execution in the
View . Note the dependencies in the source code. Each of them points inward to the
Scenario .
Usually we solve this apparent contradiction with the help of the
Principle of Inversion of Dependencies .
For example, suppose you need to refer to the
View from the
Scenario . However, this call should not be direct, in order not to violate the
Rule of Dependencies - the inner circle does not know anything about the outside. Thus, the
Scenario calls the interface (shown in the diagram as the Output Port) in the inner circle, and the
View from the outer circle implements it.
The same technique is used to cross all boundaries in architecture. We will take advantage of dynamic polymorphism to create dependencies in the source code so that the flow of control complies with the
Rule of Dependencies .
How data crosses borders.
Typically, data that crosses boundaries is simply data structures. You can use basic structures or, if you wish, Data Transfer Objects (DTO is one of the design patterns used to transfer data between application subsystems). Or the data may simply be function call arguments. Or you can pack it into a hash table or object. It is important that the transmitted data structures be isolated when passing across borders. We do not want to cheat and transmit the
Essence or DB lines. We do not want data structures to have any dependencies that violate the
Dependency Rule .
For example, many frameworks (ORM) in response to a query to the database return data in a convenient format. We could call it RowStructure. We do not want to transfer this structure across the border. This would be a violation of the
Dependencies Rule, since in this case the inner circle receives information about the outer circle.
Therefore, the transfer of data across the border should always be in a format convenient for the inner circle.
Conclusion
Following these simple rules is not difficult and they will save you a lot of time in the future. By separating the software into layers and following the
Rule of Dependencies, you will create a system to be tested, with all the attendant advantages. When any of the external subsystems becomes obsolete, be it a database or a web framework, you can easily replace them.
PS This article is translated as preparation for a later and slightly more practical oriented article on pure architecture on Go. Interesting?