Single responsibility principle, the same principle of uniform responsibility,
He is the principle of uniform variability - an extremely slippery guy for understanding 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 course, when young and green people were taken to the forest to make students-real students from the larvae.
In the forest, we were divided into groups of 8-9 people each and organized a competition - which group would drink a bottle of vodka faster, provided that the first person from the group pours vodka into a glass, drinks the second, and bites the third. The unit that performed its operation gets to the end of the group queue.
The case when the queue size was a multiple of three, and was a good implementation of the SRP.
The formal definition of the principle of common responsibility (SRP) says that each object has its own responsibility and reason for existence and this responsibility is only one.
Consider a booze object ( tippler ).
To fulfill the SRP principle, we will divide the duties into three:
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 snack.
Drinking, in turn, is a facade for these operations:
lass Tippler { //... void Act(){ _pourOperation.Do() // _drinkUpOperation.Do() // _takeBiteOperation.Do() // } }
A human programmer writes code for a monkey man, and a monkey man is inconsiderate, stupid, and always in a hurry somewhere. He can hold and understand about 3 - 7 terms at a time.
In the case of drinking these three terms. However, if we write the code of one sheet, then there will be hands, glasses, massacres and endless debates about politics. 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 sharpened to model real-world objects in his head. In his imagination he can push them, assemble new objects from them and disassemble them in the same way. Imagine an old car model. In your imagination, you can open the door, unscrew the door trim and see the window regulator mechanisms, inside which there will be gears. But you can not see all the components of the machine at the same time, in one "listing". At least "ape-man" can not.
Therefore, human programmers decompose complex mechanisms into a set of less complex and working elements. However, it is possible to decompose in different ways: in many old machines, the air duct goes out the door, and in modern ones, the lock electronics fails to prevent the engine from starting, which delivers during repair.
So, SRP is a principle explaining HOW to decompose, that is, where to draw the separation line .
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.
Let us return to the drinker and the pluses that a monkey man gets when decomposing:
And, of course, cons:
Let the gentlemen! The class of drinkers also fulfills a single responsibility - it drinks! And in general, the word "responsibility" is a very vague concept. Someone is responsible for the fate of mankind, and someone is responsible for raising penguins overturned at the pole.
Consider two implementations of drinking. The first, mentioned above, contains three classes - pour, drink and snack.
The second, written through the methodology "Forward and only forward" and contains all the logic in the Act method:
// . 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 outsider, look absolutely the same and fulfill the single responsibility of “drinking”.
Embarrassment!
Then we climb into the Internet and find out another definition of SRP - the principle of uniform variability.
This definition states that " The module has one and only one reason for change ." That is, "Responsibility is a reason for change."
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, all that can be changed is changed only in the Act method. This can be readable and efficient in the case where there is little logic and it rarely changes, but often it ends in terrible methods with 500 lines each, with the number of if -s greater than is required for Russia to join NATO.
Drinkers often do not understand why they woke up in someone else’s apartment, or where they were mobile. It's time to add detailed logging.
Let's start logging with 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}"); } }
Having encapsulated it in PourOperation , we acted wisely from the point of view of responsibility and encapsulation, but now with the principle of variability we are now confused. In addition to the operation itself, which can change, logging itself becomes changeable. It is necessary 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 meticulous reader will notice that LogAfter , LogBefore and OnError can also change separately, and by analogy with the previous actions will create three classes: PourLoggerBefore , PourLoggerAfter and PourErrorLogger .
And remembering that there are three operations for drinking, we get nine logging classes. As a result, the whole drinker consists of 14 (!!!) classes.
Hyperbola? Hardly! A monkey man with a decomposition grenade will crush the “pouring” into a decanter, a glass, pouring operators, a water supply service, a physical model of molecular collisions, and the next quarter will try to unravel dependencies without global variables. And believe me - it will not stop.
It is at this point that many come to the conclusion that the SRP are fairy tales from the pink kingdoms, and they leave the noodles ...
... without knowing about the existence of the third definition of Srp:
" Similar things for 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 should change it in one place.
This is a very important point - since all the SRP explanations that were above said that it was necessary to split up the types while they were split up, that is, they imposed a "top limit" on the object size, and now we are already talking about the "bottom limit" . In other words, the SRP not only requires "to break up while it is being crushed," but also not to overdo it - "not to crush the linked things . " Do not complicate unnecessarily. This is the great battle of Occam's razor with the ape-man!
Now the drinker should be easier. 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 the fourth type of operation, then logging is ready for it. And the code of the operations themselves is clean and free from infrastructural noise.
As a result, we have 5 classes for solving the problem of drinking:
Each of them is responsible strictly for one functionality, has one reason for the change. All similar to change the rules are next.
As part of the development of the data transfer protocol, it is necessary to do serialization and deserialization of some type of "User" into a string.
User{ String Name; Int Age; }
You might think that serialization and deserialization should be done in separate classes:
UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} }
Since each of them has its own responsibility and one reason for change.
But the reason for changing their common is “changing the format of data serialization”.
And changing this format will always change both serialization and deserialization together.
According to the principle of localization of changes, we must combine them into one class:
UserSerializer{ String deserialize(User){...} User serialize(String){...} }
This saves us from unnecessary complexity, and the need to remember that with each change of the serializer, we must also remember about the deserializer.
You must calculate the company's annual revenue and save it in the file C: \ results.txt.
Quickly solve this with one method:
void SaveGain(Company company){ // // }
Already from the definition of the task it is clear that there are two subtasks - "Calculate revenue" and "Save revenue". Each of them has one reason for the changes - "changed counting methods" and "change the format of the preservation." These changes do not overlap. Also, we cannot answer the question “what does the SaveGain method do?” In a monosyllabic way. This method counts revenue and saves results.
Therefore it is necessary to divide this method into two:
Gain CalcGain(Company company){..} void SaveGain(Gain gain){..}
Pros:
Once we wrote an auto b2b client registration service. And a GOD method appeared with 200 lines of similar content:
There were about 10 other business operations with a terrible connection in this list. The object of the account was needed by almost everyone. The point identifier and client 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 got better, but there were 100 lines of code left that didn’t want to unravel.
Only a few days later it became clear that the essence of this “easier” method is the business algorithm. And that initial description of the TK was rather complicated. And it is an attempt to break into pieces this method would be a violation of SRP, and not vice versa.
It's time to leave our drinker alone. Wipe your tears - we will definitely return to it somehow. And now we formalize the knowledge from this article.
I have not met sufficient criteria for the implementation of SRP. But there are necessary conditions:
1) Ask yourself the question - what does this class / method / module / service do? you must answer it with a simple definition. (thanks to Brightori )
But 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.
Since the responsibility (for the feature or 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 logging operations will require changing only the logger. Run around the rest of the code is not required.
Another example is the addition of a new UI control, similar to the previous ones. If this forces you to add 10 different entities and 15 different converters - it seems that you have “crushed”.
3) If several developers are working on different features of your project, then the probability 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.
If you add a new operation "Pour vodka under the table", you need to touch the logger, the operation of drinking and pouring - it seems that the responsibilities are divided crookedly. Of course, this is not always possible, but you should try to reduce this figure.
4) For a clarifying question about business logic (from a developer or manager), you climb strictly into one class / file and receive information only from there.
Features, rules or algorithms are compactly written each in one place, and not scattered flags throughout the code space.
5) Naming is understandable.
Our class or method is responsible for one thing, and responsibility is reflected in its name.
AllManagersManagerService - most likely, God-class
LocalPayment - probably not
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 give a blunder. You can be mistaken in different ways:
It is important to remember the rule: “it’s better to err on the big side,” or “not sure - don’t split up. If, for example, your class brings together two responsibilities - then it is still understandable and can be cut into two with minimal change in client code. Collecting glass from glass fragments is usually more difficult due to the context smeared over several files and the lack of necessary dependencies in the client code.
The scope of the SRP is not limited to OOP and SOLID. It is applicable to methods, functions, classes, modules, microservices, and services. It is applicable both to figax-figax-and-in-prod, and to “rocket seins” development, making the world a little better everywhere. If you think about it, then this is almost the fundamental principle of all engineering. Mechanical engineering, control systems, and indeed all complex systems are built from components, and “incomplete crafting” deprives designers of flexibility, “fragmentation” of efficiency, and incorrect boundaries — of reason and peace of mind.
SRP is not invented by nature and is not part of exact science. He gets out of our biological and psychological limitations. This is just a way to control and develop complex systems with the help of the human-monkey's brain. He tells us how to decompose the system. The original wording required a fair amount of telepathy, but I hope this article slightly dispelled the smoke screen.
Source: https://habr.com/ru/post/454290/