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.

Input Points Layer

Input Points Layer

Layer tasks:

  1. Organization of a communication channel between the client and business logic

  2. 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.

Business logic layer

Business logic layer

Layer tasks:

  1. Application of business rules

  2. Calling the transmit/receive layer

  3. 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.

Output Points Layer

Output Points Layer

Layer tasks:

  1. Organization of a communication channel with an external source of information

  2. 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:
  1. Python development patterns: TDD, DDD and event-driven architecture. Bob Gregory, Harry Percival

  2. Clean architecture. The art of software development. Martin Robert

  3. Microservices. Development and Refactoring Patterns Chris Richardson

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *