Principe de responsabilité unique, également connu sous le nom de principe de responsabilité unique,
également appelé principe de variabilité unique — un concept difficile à comprendre et un sujet délicat lors d'un entretien pour un programmeur.
Ma première rencontre sérieuse avec ce principe a eu lieu au début de ma première année, lorsque nous, jeunes et naïfs, avons été emmenés dans les bois afin de transformer des larves d'étudiants en véritables étudiants.
Dans les bois, nous avons été divisés en groupes de 8 à 9 personnes et nous avons organisé une compétition — quel groupe peut boire une bouteille de vodka le plus rapidement, sous condition que la première personne dans le groupe verse la vodka dans un verre, la deuxième boive, et la troisième morde quelque chose. L'unité ayant terminé son opération se met à l'arrière de la file du groupe.
Le cas où la taille de la file d'attente était un multiple de trois était une bonne réalisation du SRP.
Définition 1. Responsabilité unique.
La définition officielle du principe de responsabilité unique (SRP) stipule que chaque objet a sa propre responsabilité et une raison d'être, et cette responsabilité est unique.
Considérons l'objet « Buveur » (Tippler).
Pour respecter le principe SRP, nous allons diviser les responsabilités en trois :
- L'un verse (PourOperation)
- L'un boit (DrinkUpOperation)
- L'un mord (TakeBiteOperation)
Chaque participant au processus est responsable d'un composant du processus, c'est-à-dire qu'il a une responsabilité atomique — boire, verser ou mordre.
Le Buveur, pour sa part, est un façade pour ces opérations :
class Tippler {
//...
void Act(){
_pourOperation.Do() // verser
_drinkUpOperation.Do() // boire
_takeBiteOperation.Do() // mordre
}
}
Pourquoi ?
Un programmeur écrit du code pour un homme-singe, et l'homme-singe est inattentif, stupide et toujours pressé. Il peut retenir et comprendre environ 3 à 7 termes à la fois.
Dans le cas du Buveur, il y a trois termes. Cependant, si nous écrivons du code sous forme de feuille unique, celui-ci comportera des bras, des verres, des coups de poing et des débats infinis sur la politique. Et tout cela se trouvera dans le corps d'une seule méthode. Je suis sûr — vous avez déjà vu ce type de code dans votre pratique. Ce n'est pas le plus humain des défis pour la psyché.
D'une autre part, l'homme-singe est conçu pour modéliser des objets du monde réel dans son esprit. Dans son imagination, il peut les heurter, en assembler de nouveaux et les démonter de la même façon. Imaginez un vieux modèle de voiture. Vous pouvez dans votre esprit ouvrir la porte, dévisser la garniture de la porte et voir les mécanismes des lève-vitre, à l'intérieur desquels se trouvent des engrenages. Mais vous ne pouvez pas voir tous les composants de la voiture en même temps, dans un seul « listing ». Du moins, un « homme-singe » ne peut pas.
C'est pourquoi les programmeurs décomposent des mécanismes complexes en un ensemble d'éléments moins complexes et fonctionnels. Cependant, il existe différentes manières de décomposer : dans de nombreuses anciennes voitures, le conduit d'air sort dans la porte, alors que dans les modernes, une défaillance de l'électronique de la serrure empêche le moteur de démarrer, ce qui complique les réparations.
Eh bien, SRP — est un principe qui explique COMMENT décomposer, c'est-à-dire où tracer la ligne de séparation..
Il dit que la décomposition doit se faire selon le principe de séparation des « responsabilités », c'est-à-dire selon les tâches des différents objets.
Revenons à l'ivrogne et aux avantages que l'homme-singe obtient en décomposant :
- Le code est devenu extrêmement clair à chaque niveau.
- Le code peut être écrit par plusieurs programmeurs en même temps (chacun écrivant un élément distinct).
- Les tests automatisés sont simplifiés — plus un élément est simple, plus il est facile à tester.
- Une composition de code apparaît — on peut remplacer. DrinkUpOperation par une opération où l'ivrogne verse un liquide sous la table. Ou remplacer l'opération de versement par une opération où vous mélangez du vin et de l'eau ou de la vodka et de la bière. Selon les exigences de l'entreprise, vous pouvez tout faire, tout en ne touchant pas au code de la méthode. Tippler.Act.
- À partir de ces opérations, vous pouvez construire un glouton (en utilisant uniquement. TakeBitOperation), un alcoolique (en utilisant uniquement. DrinkUpOperation directement de la bouteille) et satisfaire de nombreuses autres exigences commerciales.
(Oh, il semble que ce soit déjà le principe OCP, et j'ai violé la responsabilité de ce post.)
Et bien sûr, les inconvénients :
- Il faudra créer plus de types.
- L'ivrogne boira pour la première fois quelques heures plus tard qu'il ne le pourrait.
Définition 2. Variabilité unique.
Permettez, mesdames et messieurs ! La classe du buveur a une seule responsabilité : boire ! En fait, le terme « responsabilité » est extrêmement flou. Certains sont responsables du destin de l’humanité, tandis que d’autres sont responsables de relever les pingouins tombés sur le pôle.
Considérons deux implémentations du buveur. La première, mentionnée ci-dessus, contient trois classes : verser, boire et grignoter.
La seconde, écrite selon la méthodologie « En avant et seulement en avant », contient toute la logique dans la méthode 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);
}
}
}Ces deux classes, du point de vue d'un observateur extérieur, semblent tout à fait identiques et ont la même responsabilité : « boire ».
Quelle honte !
Nous nous tournons alors sur Internet et découvrons une autre définition du SRP – le Principe de Changeabilité Unique (Single Changeability Principle).
Le SCP stipule que «Un module n’a qu’un seul et unique motif de changement». En d'autres termes, « La responsabilité est un motif de changement ».
(On dirait que les gars qui ont inventé la définition originale croyaient aux capacités télépathiques de l’homme-singe)
Maintenant tout devient clair. On peut modifier séparément les procédures de versement, de consommation et de grignotage, et dans le buveur lui-même, on ne peut changer que l'ordre et la composition des opérations, par exemple, en déplaçant le grignotage avant de boire ou en ajoutant la lecture d'un toast.
Dans l'approche « En avant et seulement en avant », tout ce qui peut être changé ne change que dans la méthode Act. C'est peut-être lisible et efficace lorsqu'il y a peu de logique et qu'elle change rarement, mais cela finit souvent par des méthodes horribles de 500 lignes chacune, avec un nombre de if supérieur à ce qui est nécessaire pour l'adhésion de la Russie à l’OTAN.
Définition 3. Localisation des changements.
Les buveurs ne comprennent souvent pas pourquoi ils se réveillent dans un appartement qui n’est pas le leur, ou où se trouve leur téléphone portable. Il est temps d'ajouter une journalisation détaillée.
Commencez la journalisation avec le processus de versement :
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Avant de verser avec {_hand} et {_bottle}");
// Logique métier de versement ...
_log.Log($"Après avoir versé avec {_hand} et {_bottle}");
}
}en l'encapsulant dans PourOperation, nous avons agi avec sagesse en termes de responsabilité et d'encapsulation, mais nous rencontrons maintenant un dilemme avec le principe de variabilité. En plus de l'opération elle-même, qui peut changer, la journalisation devient également variable. Nous devrons séparer et créer un journaliseur spécial pour l'opération de versement :
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{
//... logique métier
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e);
}
}
}Le lecteur attentif remarquera que LogAfter, LogBefore et OnError peuvent également changer indépendamment, et à l'instar des actions précédentes, cela créera trois classes : PourLoggerBefore, PourLoggerAfter et PourErrorLogger.
Et en se rappelant qu'il y a trois opérations pour le verseur, nous obtenons neuf classes de journalisation. Au total, le verseur consiste en 14 (!!!) classes.
Hyperbole ? À peine ! Un homme-singe avec une grenade de décomposition réduira le “verseur” en carafe, verre, opérateurs de versement, service d'eau, modèle physique de collision des molécules, et le prochain trimestre tentera de démêler les dépendances sans variables globales. Et croyez-moi — il ne s'arrêtera pas.
C'est précisément à ce stade que beaucoup en viennent à la conclusion que le SRP est une histoire de contes de fées issus de royaumes roses, et s'en vont fabriquer des nouilles...
... sans jamais apprendre l'existence de la troisième définition du SRP :
« Le principe de responsabilité unique stipule que les choses semblables à modifier doivent être conservées au même endroit«. ou «Ce qui change ensemble doit être conservé au même endroit”
C'est-à-dire que si nous changeons la journalisation de l'opération, nous devons le faire au même endroit.
C'est un point très important — car toutes les explications précédant le SRP parlaient de la nécessité de fractionner les types jusqu'à ce qu'ils soient complètement décomposés, imposant ainsi une « limite supérieure » à la taille de l'objet, et maintenant nous parlons également d'une « limite inférieure ». En d'autres termes, Le SRP exige non seulement de « fractionner jusqu'à ce que cela soit possible », mais aussi de ne pas en faire trop — « ne pas décomposer les choses liées ». C'est une grande bataille entre le rasoir d'Occam et l'homme-singe !
Maintenant, le verseur devrait trouver cela plus facile. En plus de ne pas avoir à décomposer le IPourLogger en trois classes, nous pouvons également combiner tous les journaliseurs en un seul type :
class OperationLogger{
public OperationLogger(string operationName){
/*..*/}
public void LogBefore(object[] args){
/*...*/}
public void LogAfter(object[] args){
/*..*/}
public void LogError(object[] args, exception e){
/*..*/}
}Et si un quatrième type d'opération venait s'ajouter, la journalisation serait déjà prête pour cela. Le code des opérations elles-mêmes est propre et débarrassé de bruit infrastructurel.
Nous avons donc 5 classes pour résoudre la tâche de la consommation :
- Opération de versement
- Opération de consommation
- Opération d'accompagnement
- Journaliseur
- Facade de consommation
Chacune d'entre elles est strictement responsable d'une seule fonctionnalité, ayant une seule raison de changement. Toutes les règles similaires pour le changement sont regroupées.
Exemple de la vie réelle
Un jour, nous avons développé un service d'enregistrement automatique pour un client B2B. Et un GOD-méthode de 200 lignes est apparue avec un contenu similaire :
- Va dans 1C et crée un compte
- Avec ce compte, rends-toi au module de paiement et crée-le là-bas
- Vérifie qu'aucun compte avec ce numéro n'est créé dans le principal le serveur
- Crée un nouveau compte
- Ajoute le résultat de l'enregistrement dans le module de paiement et le numéro 1C au service des résultats d'enregistrement
- Ajoute les informations du compte dans ce tableau
- Crée un numéro de point pour ce client dans le service des points. Passe le numéro de compte 1C à ce service.
Et il y avait environ 10 autres opérations commerciales dans cette liste avec une interconnexion terrible. L'objet compte était nécessaire presque à tous. L'identifiant du point et le nom du client étaient nécessaires dans la moitié des appels.
Après une heure de refactorisation, nous avons pu séparer le code d'infrastructure et certains détails du travail avec le compte en méthodes/classes distinctes. La méthode God a été allégée, mais il restait 100 lignes de code qui ne voulaient pas se démêler.
Ce n'est que quelques jours après que nous avons compris que l'essence de cette méthode « allégée » est en fait l'algorithme métier. Et que la description initiale du cahier des charges était plutôt complexe. Et en réalité, tenter de décomposer cette méthode constituerait une violation du SRP, et non l'inverse.
Formalisme.
Il est temps de laisser notre consommateur tranquille. Essuie tes larmes — nous reviendrons à lui un jour. Pour l'instant, formalisons les connaissances de cet article.
Formalisme 1. Définition du SRP
- Séparez les éléments de sorte que chacun d'eux soit responsable de quelque chose de unique.
- La responsabilité se traduit par « raison de changement ». Cela signifie que chaque élément n'a qu'une seule raison de changement, en termes de logique métier.
- Les éventuels changements de logique commerciale doivent être localisés. Les éléments modifiables synchroniquement doivent être à proximité.
Formalisme 2. Critères nécessaires pour l'auto-évaluation.
Je n'ai pas trouvé de critères suffisants pour la réalisation du SRP. Mais il y a des conditions nécessaires :
1) Posez-vous la question : que fait cette classe/méthode/module/service ? Vous devez y répondre par une définition simple. (merci )
explications
Cependant, parfois, trouver une définition simple est très difficile.
2) La correction d'un bug ou l'ajout d'une nouvelle fonctionnalité touche le minimum de fichiers/classes. Idéalement — un seul.
explications
Comme la responsabilité (pour la fonctionnalité ou le bug) est encapsulée dans un fichier/classe, vous savez exactement où chercher et quoi modifier. Par exemple : la fonctionnalité de modification de l'affichage des opérations de journalisation ne nécessitera de changer que le système de journalisation. Il n'est pas nécessaire de fouiller dans tout le reste du code.
Un autre exemple — l'ajout d'un nouveau contrôle UI, similaire aux précédents. Si cela vous oblige à ajouter 10 entités différentes et 15 convertisseurs différents — il semble que vous ayez "dérapé".
3) Si plusieurs développeurs travaillent sur différentes fonctionnalités de votre projet, la probabilité de conflit de fusion, c'est-à-dire la probabilité que le même fichier/classe soit modifié par plusieurs développeurs en même temps, est minimale.
explications
Si l'ajout d'une nouvelle opération «Verser de la vodka sous la table» nécessite de toucher au système de journalisation, à l'opération de consommation et de versement — alors il semble que les responsabilités soient mal réparties. Bien sûr, cela n'est pas toujours possible, mais il faut essayer de réduire ce chiffre.
4) Lors d'une question de clarification sur la logique commerciale (d'un développeur ou d'un manager), vous ne devez interroger qu'une seule classe/fichier et obtenir l'information uniquement de là.
explications
Les fonctionnalités, règles ou algorithmes sont écrits compactement, chacun à un seul endroit, et non éparpillés en drapeaux dans tout le code.
5) Le nommage est clair.
explications
Notre classe ou méthode est responsable d'une seule chose, et cette responsabilité se reflète dans son nom.
AllManagersManagerService — très probablement une classe God.
LocalPayment — probablement non.
Formalisme 3. Méthode de développement «Ockham-first».
Au début de la conception, le monkey-man ne connaît pas et ne ressent pas toutes les subtilités de la tâche à résoudre et peut faire des erreurs. On peut se tromper de différentes manières :
- Créer des objets trop grands en fusionnant différentes responsabilités
- Décomposer en divisant une seule responsabilité en plusieurs types différents
- Définir incorrectement les limites de responsabilité
Il est important de se rappeler la règle : « il vaut mieux se tromper dans le sens large », ou « si vous n'êtes pas sûr — ne décomposez pas ». Par exemple, si votre classe regroupe deux responsabilités — elle reste compréhensible et peut être séparée en deux avec un minimum de changements dans le code client. Rassembler un verre à partir de bris de verre est généralement plus difficile en raison du contexte dispersé sur plusieurs fichiers et de l'absence de dépendances nécessaires dans le code client.
Il est temps de conclure
Le domaine d'application du SRP ne se limite pas à la POO et au SOLID. Il s'applique aux méthodes, fonctions, classes, modules, microservices et services. Il est applicable aussi bien au développement « figaks-figaks-et-en-prod » qu’au développement « rocket-science », rendant partout le monde un peu meilleur. Si l'on y réfléchit, c’est sans doute un principe fondamental de toute ingénierie. La mécanique, les systèmes de contrôle et, en général, tous les systèmes complexes sont construits à partir de composants, et le « manque de décomposition » prive les concepteurs de flexibilité, tandis que « trop de décomposition » menace l’efficacité, et des limites incorrectes compromettent la rationalité et la tranquillité d'esprit.
Le SRP n'est pas une invention de la nature et ne fait pas partie des sciences exactes. Il émane de nos propres limites biologiques et psychologiques. C'est juste un moyen de contrôler et de développer des systèmes complexes à l'aide du cerveau humain-primat. Il nous indique comment décomposer un système. La formulation initiale nécessitait une certaine compétence en télépathie, mais j'espère que cet article a un peu levé le brouillard.
Source : habr.com
