Prehistory
Over the past couple of years I have participated in a considerable number of interviews. At each of them, I asked applicants about the principle of sole responsibility (hereinafter SRP). And most people know nothing about principle. And even of those who could read the definition, almost no one could say how they use this principle in their work. They could not tell how SRP affects the code they write or the review of the code of colleagues. Some of them also had the misconception that SRP, like the whole SOLID, is only related to object-oriented programming. Also, often people could not identify obvious cases of violation of this principle, simply because the code was written in the style recommended by a well-known framework.
Redux is a prime example of a framework whose guideline violates SRP.
SRP matters
I want to start with the value of this principle, with the benefit that it carries. And also I want to note that the principle applies not only to OOP, but also to procedural programming, functional and even declarative. HTML, as a representative of the latter, can also be and should be decomposed, especially now when it is controlled by UI frameworks, such as React or Angular. In addition, the principle applies to other engineering areas. And not only engineering, there was such an expression in the military theme: "divide and conquer", which by and large is the embodiment of the same principle. The difficulty kills, divide it into parts and you will win.
Regarding the other engineering areas, here, in Habré, there was an interesting article about how engines developed by the aircraft failed, they did not switch to reversal at the command of the pilot. The problem was that they misinterpreted the condition of the chassis. Instead of relying on the systems controlling the chassis, the motor controller directly read the sensors, limit switches, and so on, located in the chassis. Also in the article it was mentioned that the engine must undergo a long certification before it is even put on the prototype of the aircraft. And the violation of the SRP in this case clearly led to the fact that when changing the design of the chassis, the code in the engine controller needed to be modified and re-certified. Worse, a violation of this principle nearly cost the plane and the life of a pilot. Fortunately, our everyday programming does not have such consequences, however, it is still not worthwhile to neglect the principles of writing good code. And that's why:
- Code decomposition reduces its complexity. For example, if solving a problem requires you to write a code with cyclomatic complexity equal to four, then the method responsible for solving two such problems simultaneously requires a code with complexity 16. If this is divided into two methods, then the total complexity will be 8. Of course, this is not always reduced to the amount against the product, but the trend will be about this anyway.
- Unit testing of decomposed code becomes easier and more efficient.
- Decomposing code creates less resistance to change. When making changes, less likely to make a mistake.
- The code gets better structured. It is much easier to search for something in code laid out in files and folders than in a single large portwoman.
- Separating the boilerplate code from the logic business leads to the fact that code generation can be applied in the project.
And all these signs go together, these are signs of the same code. You do not need to choose between, for example, well-tested code and well-structured.
Existing definitions do not work.
One of the definitions is: “there must be only one reason for changing the code (class or function)”. The problem with this definition is that it conflicts with the Open-Close principle, the second of a group of principles SOLID. Its definition is: “the code must be open for extension and closed for change”. One reason for the change is against a complete ban on change. If we disclose in more detail what is meant here, it turns out that there is no conflict between the principles, but there is definitely a conflict between fuzzy definitions.
')
The second, more direct definition is: “the code should have only one responsibility”. The problem with this definition is that it is human nature to generalize everything.
For example, there is a farm that grows chickens, and at this moment the farm has only one responsibility. And so the decision is made to breed ducks there as well. Instinctively, we will call it a farm for poultry, rather than admit that there are now two responsibilities. We will add sheep there, and this is now a farm for pets. Then we want to grow tomatoes or mushrooms there, and come up with the following even more generalized name. The same applies to "one reason" for change. This reason can be as generalized as far as imagination is enough.
Another example is the class manager of a space station. He does not do anything else, only controls the space station. How do you like such a class with one responsibility?
And, since I mentioned Redux, when the applicant is familiar with this technology, I also ask the question, but does the typical SRP reducer break?
A reducer, I remind you, includes a switch statement, and it happens that it grows to dozens and even hundreds of cases. And the sole responsibility of the reducer is to manage the transitions of the state of your application. Exactly so, literally, some applicants answered. And no hint could get this opinion off the ground.
So, if some code seems to satisfy the SRP principle, but it also “smells” unpleasantly - know why this is happening. Because the definition of “code must have one responsibility” simply does not work.
More appropriate definition
From trial and error, I had a better definition:
Code liability should not be too big.Yes, now you need to "measure" the responsibility of the class or function. And if it is too large, then you need to break this big responsibility into several smaller responsibilities. Returning to the farm example, even the responsibility for breeding chickens may be too big and it makes sense to separate the broilers from the hens, for example.
But how to measure it, how to determine that the responsibility of this code is too great?
Unfortunately, I do not have mathematically exact methods, there are only empirical ones. And most of all it comes with experience, novice developers are not able to decompose the code at all, the more advanced ones own it better, although they cannot always describe why they do it and how it falls on theories like SRP.
- Metric cyclomatic complexity. Unfortunately, there are ways to mask this metric, but if you collect it, then there is a chance that it will show the most vulnerable points of your application.
- The size of the functions and classes. The function of 800 lines does not need to be read in order to understand that something is wrong with it.
- A lot of imports. Once I opened the file in the project of the neighboring team and saw the whole import screen, clicked page down and again there were only imports on the screen. Only after the second pressing I saw the beginning of the code. You can say that all modern IDEs can hide imports under the “plus sign”, but I say that a good code does not need to hide “smells”. In addition, I needed to reuse a small piece of code, and I took it out of this file to another one, and a quarter or even a third of imports moved behind this piece. This code was clearly not the place.
- Unit Tests. If you still have difficulty determining responsibility, force yourself to write tests. If the main purpose of the function is to write two dozen tests, not counting borderline cases, etc., then decomposition is necessary.
- The same applies to too many preparatory actions at the beginning of the test and checks at the end. On the Internet, by the way, you can find a utopian statement that the so-called. Assert in the test in general there should be only one. I believe that any arbitrarily good idea, being elevated to the absolute, can become impractical until it is impractical.
- Business logic should not be directly dependent on external tools. The Oracle driver, Express routes, it is desirable to separate all this from the logic business and / or hide behind the interfaces.
A couple of points:
Of course, as I have already mentioned, there is a flip side to the coin, and 800 methods on one line may not be better than one method on 800 lines, there must be a balance in everything.
Secondly, I do not cover the question of where to put this or that code in accordance with its responsibility. For example, sometimes developers also have difficulty with pulling too much logic into the DAL layer.
Third, I do not propose any specific hard constraints like “no more than 50 lines per function”. This approach assumes only a direction for the development of developers, and perhaps teams. He works for me, must earn for others.
And finally, if you go through TDD, this alone will surely force you to decompress the code long before you write those 20 tests with 20 asserts in each.
Separating business logic from boilerplate code
Talking about the rules of good code can not be dispensed with examples. The first example is dedicated to the separation of the boilerplate code.

This example demonstrates how the back-end code is usually written. People usually write the logic inseparably from the code that specifies the Express Web server to include parameters such as URL, request method, etc.
I marked the business logic with a green marker, and a red blotch of code interacting with the query parameters (red).
I always share these two responsibilities in this way:

In this example, all interaction with Express is transferred to a separate file.
At first glance it may seem that the second example did not bring any improvements, there were 2 files instead of one, there were additional lines that were not there before - the name of the class and the signature of the method. And what then does such a code separation give? First of all, the “application entry point” is no longer Express. Now this is the usual Typescript function. Or javascript function, whether C #, who writes on what WebAPI.
This in turn allows you to perform various actions that are not available in the first example. For example, you can write behavior tests without the need to raise Express, without using http requests inside the test. And even there is no need to make any mocking, replace the Router object with its “test” object, now the application code can be simply called from the test directly.
Another interesting feature that this decomposition provides is that now you can write a code generator that will parse the userApiService and, based on it, generate the code linking this service to Express. In my future publications, I plan to indicate the following: code generation will not save time in the process of writing code. The cost of the code generator will not pay off by the fact that now you do not need to copy-paste this boilerplate. Code generation will pay off by the fact that the code generated by it does not need support, which will save time and, most importantly, the nerves of developers in the long run.
Divide and conquer
This method of writing code has been around for a long time; I did not invent it myself. I just came to the conclusion that it is very convenient when writing business logic. And for this, I came up with another fictional example that shows how to quickly and easily write code that is immediately well decomposed, and also self-documented by naming methods.
Let's say you get a task from a business analyst to make a method that sends a employee report to an insurance company. For this:
- Data needs to be taken from the database
- Convert to the desired format
- Send the resulting report
Such requirements are not always written explicitly, sometimes such a sequence may be implied or clarified from a conversation with an analyst. In the process of implementing the method, do not rush to open connections to the database or network; instead, try to translate this simple algorithm into code “as is”. Like that:
async function sendEmployeeReportToProvider(reportId){ const data = await dal.getEmployeeReportData(reportId);​ const formatted = reportDataService.prepareEmployeeReport(data);​ await networkService.sendReport(formatted);​ }
With this approach, it turns out quite simple, easy to read and test code, although I believe that this code is trivial and does not need testing. And the responsibility of this method is not to send a report, its responsibility is to split this complex task into three subtasks.
Next, we return to the requirements and find out that the report should consist of a salary section and a section with hours worked.
function prepareEmployeeReport(reportData){ const salarySection = prepareSalarySection(reportData);​ const workHoursSection = prepareWorkHoursSection(reportData);​ return { salarySection, workHoursSection };​ }
And so on, we continue to break the task until the implementation of small methods that are close to trivial remains.
Interaction with the open-close principle
At the beginning of the article I said that the definitions of the principles of SRP and Open-Close contradict each other. The first one says that there should be one reason for the change, the second one says that the code should be closed for the change. And the principles themselves, not only do not contradict each other, on the contrary, they work in synergy with each other. All 5 principles of SOLID are aimed at one good goal - to tell the developer what code is “bad” and how to change it so that it becomes “good”. The irony is that I have just replaced 5 responsibilities with one more responsibility.
So, in addition to the previous example, sending a report to an insurance company, let's imagine that a business analyst comes to us and says that now we need to add a second functionality to the project. The same report should be printed.
Imagine that there is a developer who believes that SRP is “not about decomposition”.
Accordingly, this principle did not indicate the need for decomposition, and he implemented the entire first task in one function. After the task came to him, he unites two responsibilities into one, since there is much in common between them and summarizes its name. Now this responsibility is called “serve the report”. The implementation of this looks like this:
async function serveEmployeeReportToProvider(reportId, serveMethod){ switch(serveMethod) { case sendToProvider: case print: default: throw; } }
Does it remind any code in your project? As I said, both direct SRP definitions do not work. Do not convey to the developer information that such code can not be written. And on what kind of code you can write. For the developer, there is still only one reason left to change this code. He just re-called the previous reason, added a switch and was calm. And here the principle of the Open-Close principle comes on the scene, which directly says that it was impossible to change an existing file. It was necessary to write the code so that when adding new functionality, it was necessary to add a new file, and not edit the existing one. That is, such code is bad from the point of view of two principles at once. And if the first did not help to see it, the second should help.
And how the “divide and conquer” method solves this problem:
async function printEmployeeReport(reportId){ const data = await dal.getEmployeeReportData(reportId);​ const formatted = reportDataService.prepareEmployeeReport(data);​ await printService.printReport(formatted);​ }
Add a new feature. I sometimes call them “function-script”, because they do not carry the implementation, they determine the sequence of calling the decomposed pieces of our responsibility. Obviously, the first two lines, the first two decomposed responsibilities coincide with the first two lines of the previously implemented function. Just as the first two steps of the two tasks described by the business analyst coincide.
Thus, to add new functionality to the project, we added a new script method and a new printService. Old files have not been changed. That is, this method of writing code is good immediately from the position of two principles. Both SRP and Open-Close
Alternative
I also wanted to mention an alternative, competing way to get well decomposed code that looks something like this - first write the code head-on, then refactor it using various techniques, for example, using Fowler's book Refactoring. These methods reminded me of a mathematical approach to the game of chess, where you do not understand what exactly you are doing in terms of strategy, you only calculate the "weight" of your position and try to maximize it by making moves. I didn’t like this approach for one small reason - it’s already difficult to name methods and variables, and when they don’t have a business value, it becomes impossible. For example, if these techniques suggest that you need to select 6 identical lines from here and from there, then selecting them, how to call this method? someSixIdenticalLines ()?
I want to make a reservation - I do not think this method is bad, I just could not learn to use it.
Total
In following the principle you can find a benefit.
The definition of “must be one responsibility” does not work.
There is a better definition and a number of indirect signs, so-called. code smells, signaling the need to decompose.
The “divide and conquer” approach allows you to immediately write well-structured and self-documenting code.