Single Responsibility Principle. Not as simple as it seems

Single Responsibility Principle. Not as simple as it seems Single responsibility principle, he is the principle of single responsibility,
he is the principle of single variability - an extremely slippery guy to understand and such a nervous question at a programmer's interview.

The first serious acquaintance with this principle took place for me at the beginning of the first year, when we were taken young and green into the forest to make students out of larvae - real students.

In the forest, we were divided into groups of 8-9 people each and had a competition - which group will drink a bottle of vodka faster, provided that the first person from the group pours vodka into a glass, the second one drinks, and the third one has a snack. The unit that has completed its operation is placed at the end of the group's queue.

The case where the queue size was a multiple of three was a good SRP implementation.

Definition 1. Single responsibility.

The official definition of the Single Responsibility Principle (SRP) says that each object has its own responsibility and reason for existence, and it has only one responsibility.

Consider the "Drinker" object (Tippler).
To fulfill the SRP principle, we will divide the responsibilities into three:

  • One pours (PourOperation)
  • One is drinkingDrinkUpOperation)
  • One bites (TakeBiteOperation)

Each of the participants in the process is responsible for one component of the process, that is, it has one atomic responsibility - to drink, pour or have a snack.

The boozer, in turn, is a facade for these operations:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Single Responsibility Principle. Not as simple as it seems

What for?

A human programmer writes code for a human ape, but a human ape is inattentive, stupid and always in a hurry somewhere. He can hold and understand about 3 - 7 terms at one time.
In the case of a drunkard, there are three of these terms. However, if we write the code with one sheet, then hands, glasses, scuffles and endless debates about politics will appear in it. And all this will be in the body of one method. I am sure you have seen such code in your practice. Not the most humane test for the psyche.

On the other hand, the ape-man is imprisoned for modeling real-world objects in his head. In his imagination, he can push them together, collect new objects from them and disassemble them in the same way. Imagine an old model car. You can imagine opening the door, unscrewing the door trim and seeing the power window mechanisms there, inside of which there will be gears. But you can't see all the components of a machine at the same time, in one "listing". At least the "monkey man" can't.

Therefore, human programmers decompose complex mechanisms into a set of less complex and working elements. However, it can be decomposed in different ways: in many old cars, the air duct goes out the door, and in modern ones, a failure of the lock electronics prevents the engine from starting, which delivers during repairs.

And so, SRP is a principle that explains HOW to decompose, that is, where to draw the line of separation.

He says that it is necessary to decompose according to the principle of division of "responsibility", that is, according to the tasks of certain objects.

Single Responsibility Principle. Not as simple as it seems

Let's get back to the booze and the benefits that the monkey man gets when decomposing:

  • The code became extremely clear at every level
  • Code can be written by several programmers at once (each one writes a separate element)
  • Simplifies automated testing - the simpler the element, the easier it is to test it
  • Code composition appears - you can replace DrinkUpOperation to an operation in which a drunkard pours liquid under the table. Or replace the pouring operation with an operation in which you mix wine and water or vodka and beer. Depending on the requirements of the business, you can do everything without touching the method code. Tippler.Act.
  • From these operations, you can add up the glutton (using only TakeBitOperation), Alcoholic (using only DrinkUpOperation directly from the bottle) and meet many other business requirements.

(Oh, it seems this is already an OCP principle, and I violated the responsibility of this post)

And, of course, the cons:

  • More types will have to be created.
  • Drinker will drink for the first time a couple of hours later than he could

Definition 2. Single variability.

Let me gentlemen! The drinker class also has a single responsibility - it drinks! In general, the word "responsibility" is an extremely vague concept. Someone is responsible for the fate of mankind, and someone is responsible for raising penguins capsized at the pole.

Let's consider two implementations of a drunkard. The first, mentioned above, contains three classes - pour, drink and eat.

The second one is written using the Forward and Only Forward methodology and contains all the logic in the method Act:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

Both of these classes, from the point of view of an outside observer, look exactly the same and carry out the single responsibility of "drink".

Embarrassment!

Then we climb on the Internet and find out another definition of SRP - the Single Changeability Principle.

The SCP states that "A module has one and only one reason to change". That is, "Responsibility is a reason for change."

(It looks like the guys who came up with the original definition were confident in the telepathic abilities of the ape man)

Now everything falls into place. Separately, you can change the procedures for pouring, drinking and snacking, and in the drinker itself, we can only change the sequence and composition of operations, for example, by moving the snack before drinking or adding a toast reading.

In the “Forward and Only Forward” approach, everything that can be changed is changed only in the method Act. This can be readable and efficient when there is little logic and it rarely changes, but it often ends up in terrible methods of 500 lines each, with more if s than is required for Russia to join NATO.

Definition 3. Localization of changes.

Drinkers often do not understand why they woke up in someone else's apartment, or where their mobile is. It's time to add verbose logging.

Let's start logging from the pouring process:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

By encapsulating it in PourOperation, we acted wisely in terms of responsibility and encapsulation, but now we have embarrassment with the principle of variability. In addition to the operation itself, which can change, the logging itself becomes changeable. You will have to separate and make a special logger for the pouring operation:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

The discerning reader will notice that LogAfter, LogBefore и OnError can also change individually, and by analogy with the previous steps will create three classes: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.

And remembering that there are three operations for a drunkard, we get nine logging classes. As a result, the whole drunkard consists of 14 (!!!) classes.

Hyperbola? Hardly! A monkey man with a decomposition grenade will break up the “pourer” into a decanter, a glass, pouring operators, a water supply service, a physical model of molecular collision, and the next quarter will try to unravel the dependencies without global variables. And trust me, he won't stop.

It is at this moment that many people come to the conclusion that SRP are fairy tales from the pink kingdoms, and they leave to spin noodles ...

... never knowing about the existence of the third definition of Srp:

The Single Responsibility Principle states that things that are similar to change should be kept in one place". or "What changes together should be kept in one place"

That is, if we change the operation logging, then we must change it in one place.

This is a very important point - since all the explanations of SRP that were above said that it was necessary to split types while they were split, that is, it imposed a "limit from above" on the size of the object, and now we are talking about "limit from below" . In other words, SRP not only requires "crush while it is crushing", but also not to overdo it - "do not crush linked things". It's the great battle of Occam's razor with the ape man!

Single Responsibility Principle. Not as simple as it seems

The boozer should feel better now. In addition to not splitting the IPourLogger logger into three classes, we can also combine all loggers into one type:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

And if we add a fourth type of operation, then logging is already ready for it. And the code of the operations themselves is clean and free from infrastructure noise.

As a result, we have 5 classes for solving the drinking problem:

  • pouring operation
  • Drinking Operation
  • Jam operation
  • logger
  • Drinker facade

Each of them is responsible for strictly one functionality, has one reason for changing. All similar rules for changing are nearby.

Real life example

Once we wrote a service for automatic registration of a b2b client. And a GOD method appeared for 200 lines of similar content:

  • Go to 1C and open an account
  • With this account, go to the payment module and start it there
  • Check that an account with such an account is not created in the main server
  • Create a new account
  • Add the registration result in the payment module and the number 1c to the registration results service
  • Add account information to this table
  • Create a point number for this customer in the point service. Pass the account number 1c to this service.

And there were about 10 more business operations with terrible connectedness on this list. Almost everyone needed an account object. The point ID and customer name were needed in half of the calls.

After an hour of refactoring, we were able to separate the infrastructure code and some of the nuances of working with the account into separate methods / classes. The God method became easier, but there were 100 lines of code left that they didn’t want to unravel.

Only a few days later came the understanding that the essence of this “easier” method is the business algorithm. And that the original description of the TK was quite complicated. And it is precisely the attempt to break this method into pieces that will be a violation of the SRP, and not vice versa.

Formalism.

It's time to leave our drunkard alone. Dry your tears - we will definitely return to him sometime. And now we formalize the knowledge from this article.

Formalism 1. Definition of SRP

  1. Separate the elements so that each of them is responsible for one thing.
  2. Responsibility stands for "reason for change". That is, each element has only one reason to change, in terms of business logic.
  3. Potential business logic changes. must be localized. Elements that change synchronously must be side by side.

Formalism 2. Necessary criteria for self-examination.

I did not meet sufficient criteria for fulfilling the SRP. But there are necessary conditions:

1) Ask yourself what this class/method/module/service does. you must answer it with a simple definition. ( Thank you Brightori )

explanations

However, sometimes it is very difficult to find a simple definition.

2) Fixing some bug or adding a new feature affects the minimum number of files/classes. Ideally one.

explanations

Since the responsibility (for a feature or a bug) is encapsulated in one file/class, you know exactly where to look and what to edit. For example: the feature of changing the output of the operation log will require changing only the logger. You don't need to run through the rest of the code.

Another example is adding a new UI control similar to the previous ones. If this forces you to add 10 different entities and 15 different converters, it looks like you've "oversharpened".

3) If several developers are working on different features of your project, then the likelihood of a merge conflict, that is, the likelihood that the same file / class will be changed by several developers at the same time, is minimal.

explanations

If, when adding a new operation “Pour vodka under the table”, you need to affect the logger, the operation of drinking and pouring, then it seems that the division of responsibilities is crooked. Of course, this is not always possible, but you need to try to reduce this figure.

4) When you ask a clarifying question about business logic (from a developer or manager), you climb strictly into one class / file and get information only from there.

explanations

Features, rules or algorithms are written compactly in one place, and not scattered with flags throughout the code space.

5) Naming is clear.

explanations

Our class or method is responsible for one thing, and the responsibility is reflected in its name.

AllManagersManagerService - most likely a God class
LocalPayment - probably not

Formalism 3. Occam-first development methodology.

At the beginning of the design, the monkey man does not know and does not feel all the subtleties of the problem being solved and can make a mistake. You can go wrong in different ways:

  • Make objects too big by gluing together different responsibilities
  • Reshape by dividing a single responsibility into many different types
  • Incorrectly define the boundaries of responsibility

It is important to remember the rule: “it’s better to make a mistake in a big way”, or “if you’re not sure, don’t split it up”. If, for example, your class collects two responsibilities, then it is still understandable and can be split into two with minimal changes to the client code. As a rule, it is more difficult to assemble a glass from glass fragments due to the context spread over several files and the lack of necessary dependencies in the client code.

It's time to wrap up

The scope of SRP is not limited to OOP and SOLID. It is applicable to methods, functions, classes, modules, microservices and services. It applies to both “figax-figax-and-to-prod” and “rocket-science” development, making the world a little better everywhere. If you think about it, this is almost the fundamental principle of all engineering. Mechanical engineering, control systems, and in general all complex systems are built from components, and “under-crushing” deprives designers of flexibility, “re-crushing” - efficiency, and incorrect boundaries - reason and peace of mind.

Single Responsibility Principle. Not as simple as it seems

SRP is not invented by nature and is not part of an exact science. It crawls out of our biological and psychological limitations with you. It's just a way to control and develop complex systems using the brain of a human-ape. It tells us how to decompose the system. The original wording required a fair amount of telepathy skill, but I hope this article cleared the smokescreen a bit.

Source: habr.com

Add a comment