OOP does not define the architecture of the project

This material was originally planned as a lesson in a PHP course on polymorphism. But it eventually outgrew the lesson itself, and I decided to make a separate article out of it. There is practically nothing PHP-specific in it, so it is recommended for everyone to read.

Let me remind you that the PHP class model is taken from Java. The presence of interfaces and all the accompanying elements greatly affects the way code is organized in PHP. This method often differs from how code is organized in JavaScript, Ruby or Python. And even more so from languages ​​such as Clojure or Elixir. And all this against the background of the fact that each of these languages ​​has OOP.

OOP in these languages ​​is so different that PHP programmers who get into Ruby or JavaScript do not understand how it is possible to write like that, because many approaches contradict their ideas about the world. The same thing happens in the opposite situation.

So where is the truth? The truth is that there are things that really determine the architecture of the code. And it is not the structure of classes, not the presence of interfaces, and not the use of polymorphism.

Let's take the same MVC. It talks about layers, their tasks (areas of responsibility) and the way they interact with each other. This is extremely important for modularity. There are no cyclic dependencies in a modular system. MVC says nothing about classes and OOP in general, because there is no connection between these concepts. MVC can be implemented in any general-purpose language, no matter what it is. The same can be said about all other architectural patterns.

Architecture is based on the features of the environment in which it is used, not on the constructs of the language. For example, the web is dominated by HTTP, which is built around the concept of “request-response.” This is why microframeworks in different languages ​​look so similar, regardless of whether they have OOP or not: each microframework has a request, a response, and a response handler.

In modern development, OOP has become something like cargo cult. This has a negative effect on immature minds. If you google the query “which pattern to use”, you can find a lot of interesting and sad things:

There is a similar situation in another place of the code, which in the end I don't really like, because it looks very clumsy. I feel that a pattern is needed, but I don't even know which one to apply here. The thought crept in towards a strategy, but I'm not sure how to apply it here. Pattern gurus, advise me what to do.

Questions like these only arise from a lack of understanding of what a person is doing, and trying to cover up the problem with a pattern will cost the project dearly.

This does not mean that you don't need to know OOP and patterns. You do, especially if you work in class languages, but this is just one of many and not the most critical parts. The same OOP patterns (these are not architectural patterns, for example, MVC is not an OOP pattern) are more often used in local situations. That is, their influence on architecture is very limited.

Code architecture

Below we will try to figure out how to write code, what to pay attention to and what comes after what in order of importance.

Main parts of the application

A whole book could be written on each aspect listed below. Find many exceptions and special situations in which these approaches are not applicable. Or they are applicable, but with many clarifications… Please understand and forgive me.

Domain Model

At the core of any system is a subject area model. This is the algorithmic part of the program (the place where calculations occur), which reflects the business task. For example, in Hexlet, the subject area is an educational system. Within this area, there are concepts such as “course”, “lesson”, “profession”, “students”. All of them can interact according to certain rules.

Before you start coding a domain, it's a good idea to understand it, to imagine the set of entities it works with and the relationships between them. However, on the web, a domain model is not designed “from start to finish” before coding. Usually, only the piece that is currently being worked on is taken and only that piece is transferred to the code. Later, other parts are added as needed.

Entities in the code are represented by different structures, here everything depends on the language. In some languages ​​these are structures, in others – objects, in others – records. But no matter what method and language are chosen, the user will remain a user, and the course – a course! And for this it is not necessary to have objects.

This part, ideally, knows nothing about the usage environment, whether we are in a browser or on the backend or inside an ATM. This way modularity is achieved, the system is divided into layers that are closed with respect to their operations and do not know about the environment. In practice, achieving complete isolation is difficult and, most likely, unnecessary. But it is necessary to avoid “abstraction leakage”, that is, the top-level layer knows about the layer lying below it (and uses it), but the lower layer cannot know how it is used. In the Hexlet example, the code inside the layer responsible for operations with the exchange rate cannot know about the existence of http and the web framework.

Wednesday

The next important part is the execution environment. It defines the basic architecture of the application. If we work in a browser, then this is an event model, if in the backend via http, then “request-response”, and in the command line, direct code execution. There are other environments with their own characteristics. For each of them, a large number of architectural approaches have been developed that do not need to be invented – they are already implemented in frameworks. First of all, this is MVC. Moreover, depending on the environment, either MVC1 or MVC2.

To understand the rules of work in this layer well, you need to know operating systems and networks. For example, it is impossible to build a good API without knowing the HTTP protocol, without having an idea of ​​idempotency and message delivery guarantees in distributed systems.

Basic principles of code structuring

Isolate side effects from clean code

Everything related to input/output should not be inside, but preferably at the very top level. Moreover, most often at the beginning of the program's work, the necessary data is read, then – a large block of the main logic (clean code) and at the output – again a side effect, for example, writing to a file. On the web, this is “request-response”.

Watch for idempotency

If an operation can be implemented in such a way that it can be restarted in case of an error and this will not lead to problems, then the operation should be implemented in this way. This applies primarily to periodic and asynchronous tasks.

Use automata-based programming

Flag programming is an indicator of code that needs to be rewritten. Automata can be used very widely. In fact, any process that occurs within a system is a potential finite automaton. For example:

  • User registration (waiting for email confirmation, confirmed, banned)

  • Article publication (draft, published, deleted)

Avoid global variables

They can be objects and classes that have an internal state that can change during the life of the application.

Avoid unnecessary state and shared state

The first is especially common when objects are given internal state in situations where it is not needed. For example, when storing intermediate data between different calls. Use a formal method to check whether the internal state is needed in a given situation or not. Check whether the object performing the operation can be replaced by a function. And if the answer is “yes”, then there should be no state (except for configuration).

Extract abstractions as needed

One of the key rules in learning programming: do not do anything extra until it starts to hurt. Break down and highlight only when you feel that the current state of affairs prevents you from being effective. Only in this case will you understand when it is worth doing something and when it is not. Otherwise, it is very easy to cross the line and turn into an “architectural astronaut” (overengineering)

  • Should the function be moved to a separate file? No.

  • Do we need to make many small functions? No.

  • Should the component be broken down into components? No.

  • Is it necessary to put an index in the database just in case, since there will be a lot of data later? No.

It may be counterintuitive. But it is much easier to change code that is in one place and not broken into many small parts. It makes sense to break it up when the architecture has “settled down” and all the edge cases have been taken into account. Until then, let it be indivisible.

Isolate technical debt

Not all technical debt grows. If the abstraction is good and does not leak, then it does not matter how the code inside is written. It can be rewritten when the time comes. Sometimes the time comes — and the code is simply deleted as unnecessary. A simple example: the array sorting function.

Break your application into layers

Use layered design. Lower layers should not know about the entities of the upper layers, and the upper layers work on the basis of the lower ones. Example partitions.

Don't put performance first

Before talking about performance, read optimization guide

Similar Posts

Leave a Reply

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