Code organization is important and easy based on Layer Architecture
In what cases will this article be useful?
If you don't know how to start implementing a project
If you encounter a problem when creating a file in your project
If you don't want your code to turn into spaghetti in a short period of time
If you like neatness not only on your computer desktop, but also in writing code
The article will cover the following topics:
Why is code organization so important and what is a programmer’s task?
The question is quite trivial, but after a year of development, the state of the code sometimes resembles the picture below.
Turning code into “spaghetti” brings with it more problems than unreadability or speed, namely: the time spent on implementing business tasks, finding and fixing bugs in the project becomes too large.
The choice of architectural approach is a very controversial topic. While working at one of the companies, I spent many hours discussing with the team, but in the end we came to the conclusion that none of the architectures mentioned in the introduction were fully implemented; some improvements and clarifications were required everywhere. We forgot that one of the most important tasks of a developer is to write code so that it is easy to read by other developers and scalable to quickly implement future business problems.
I came to the conclusion that core architectures are just basic knowledge on the basis of which we build our own architectures, which, in turn, should solve the required problems, and well-organized code helps to increase the speed of development.
Layered architecture as a basic choice
Any project starts with gathering requirements, designing diagrams, etc., but we will skip these steps, assuming we have done everything necessary and start writing code.
The designed diagram displays the entry points into the application, the processing of business rules, information, and interaction with other applications. The elements of the diagram are the very layers of the architecture through which your information moves. Thus, layered architecture is easy to use and easy to understand and understand for most developers.
But using this architecture can generate many abstractions that create dependencies between layers. This will happen if you make the following mistakes in your application:
Input Points Layer
Let's consider an example of implementing an interface in the form of a call to “handles”, where the user sends a request to create an entity.
@router.post("/api/v1/entities/", name="create_entity", status_code=status_code.HTTP_201_CREATED)
def create(self, entity: api_schemas.DTO()) -> api_schemas:
return api_schemas.from_business_layer(business_layer.create(entity=entity.to_service_model())) # обращение к слою сервисов с преобразованием моделей
Why is it implemented this way? Yes, everything is obvious! When testing the “communication with the client” interface, business logic is encapsulated, which allows testing within this layer only, as well as flexible management of interface types, be it API, CLI or Crontab Task. The interface is the entry point into our application, which organizes a communication channel between the client and the business logic, and it is the responsibility for transforming models.
Layer tasks:
Organization of a communication channel between the client and business logic
Validation, serialization, deserialization of input-output data, use of various middleware
Business logic layer
This layer is responsible for managing information based on business rules. Let's look at an example:
def create(dto: bll_schemas.DTO()) -> bll_schemas.dto:
with database.start_session():
if database.get(**object):
raise bll_exc.DuplicateError("Object is already created")
database_entity = database_models.Entity(**object.model_dump())
database.create(session=session, object=database_entity)
return self.service_schema.from_orm(database_entity)
As you can see in the example, there is a rule that an entity must be created if it does not exist. To do this, we go to the database for information based on the input data and check whether this entity is there. Work with the database is, accordingly, encapsulated. This is necessary in order to test only the business rules and how they affect the management of entities, but not to test the operation of the database. This separation allows you to describe business rules in one place and not spread them throughout the application.
Layer tasks:
Application of business rules
Calling the transmit/receive layer
Validation, serialization, deserialization of input-output data
Output Points Layer
He is responsible for organizing and setting up a communication channel with an external data source. Example:
def create(entity: Entity, session: Session) -> Entity:
try:
session.add(entity)
session.commit()
session.refresh()
except DatabaseException as exc:
session.rollback()
raise exc
return entity
Above we described how create from business logic for a database could look like. We hid all the complexity of database management in a separate interface method, simplifying the business logic.
When physically managing information using interaction channels, we begin to transmit information externally and manage this channel.
Layer tasks:
Organization of a communication channel with an external source of information
Communication channel management
Scalability and a little about unit testing
There are situations when, with the advent of new requirements for a project, the layers discussed above are no longer enough to solve problems, so they become more complex and large.
Description of the example
Let's imagine that our application already has Create and Delete handles that allow you to create and delete a specific object. A business comes to us and asks for a pen to recreate an object (Recreate). Since we have part of the ready-made functionality, for this we need to first call Delete and then Create. In addition, additional conditions may appear that require access to other applications, so we will need to describe additional data processing. As a result, the task will be implemented, but each such feature leads to an increase in the size of the code both inside the handles and in the business logic, which I would like to avoid so that the code remains more concise and easier to understand.
Therefore, below I will provide you with a description of additional layers that help make the code more readable and scaling less painful.
When using multiple business logic methods, a composite layer is added that is responsible for creating various sessions and calling these methods, which avoids complex code in the input point layer and does not give it responsibility for managing sessions.
Many calls to other applications are resolved by internal business logic functions, but each such call requires additional information processing, so to prevent business logic from growing, data processing can be separated into a separate layer.
A little about Unit tests. Each team agrees differently on how to cover the code with tests. Often, 100% coverage does not bring any benefit, but only increases the code base and adds routine to writing tests. Since some functionality changes rarely, it can be tested manually, but there are things that do not need to be checked as part of Unit tests, for example, direct interaction with the database. Therefore, it is a useful practice to test business logic with stubs for external calls, since this is where the main part of the application logic lies, which needs to be developed and changed frequently.
Conclusion
Ultimately, if you use the recommendations above, you will get the diagram below (I excluded data conversion for each layer for simplicity):
This is a base from which you can build on to develop your own architecture and patterns in the team. This architecture provides development flexibility, cleaner code, clear distribution of responsibilities, and faster project development speed.
As a small bonus, I recommend books that helped me on my developer journey:
Python development patterns: TDD, DDD and event-driven architecture. Bob Gregory, Harry Percival
Clean architecture. The art of software development. Martin Robert
Microservices. Development and Refactoring Patterns Chris Richardson