Principe de responsabilité unique, il est le principe de responsabilité unique,
il est le principe de la variabilité uniforme - un gars extrêmement glissant à comprendre et une telle question nerveuse lors d'une interview d'un programmeur.
La première connaissance sérieuse de ce principe a eu lieu pour moi au début de la première année, lorsque les jeunes et les verts ont été emmenés dans la forêt pour faire de vrais élèves des larves.
Dans la forêt, nous avons été divisés en groupes de 8 à 9 personnes chacun et avons organisé un concours - quel groupe va boire une bouteille de vodka plus rapidement, à condition que la première personne du groupe verse de la vodka dans un verre, les deuxièmes boissons et les troisièmes bouchées. Après avoir terminé son opération, l'unité se trouve à la fin de la file d'attente de groupe.
Le cas où la taille de la file d'attente était un multiple de trois et constituait une bonne implémentation de SRP.
Définition 1. Responsabilité unique.
La définition officielle du principe de responsabilité unique (PRS) suggère que chaque objet a sa propre responsabilité et raison d'être, et cette responsabilité n'en a qu'une.
Considérez l'objet Tippler.
Pour respecter le principe du PÉR, nous divisons les responsabilités en trois:
- On verse ( PourOperation )
- Une boisson ( DrinkUpOperation )
- Une collation ( TakeBiteOperation )
Chacun des participants au processus est responsable d'une composante du processus, c'est-à-dire qu'il a une responsabilité atomique - boire, verser ou mordre.
L'alcool, à son tour, est la façade de ces opérations:
lass Tippler { //... void Act(){ _pourOperation.Do() // _drinkUpOperation.Do() // _takeBiteOperation.Do() // } }
Pourquoi?
Le programmeur humain écrit le code pour l'homme singe, et l'homme singe est inattentif, stupide et toujours pressé quelque part. Il peut tenir et comprendre environ 3 à 7 termes à la fois.
Dans le cas de l'alcool, ces termes sont trois. Cependant, si nous écrivons le code sur une seule feuille, des mains, des lunettes, des massacres et des débats sans fin sur la politique y apparaîtront. Et tout cela sera dans le corps d'une méthode. Je suis sûr que vous avez vu un tel code dans votre pratique. Pas le test le plus humain pour la psyché.
D'un autre côté, l'homme singe est emprisonné pour avoir modelé des objets du monde réel dans sa tête. Dans son imagination, il peut les rapprocher, en collecter de nouveaux objets et les démonter de la même manière. Imaginez un vieux modèle de voiture. Vous pouvez ouvrir la porte dans votre imagination, dévisser la garniture de porte et voir les mécanismes de lève-vitre, à l'intérieur desquels il y aura des engrenages. Mais vous ne pouvez pas voir tous les composants de la machine en même temps, dans une seule "liste". Au moins, "l'homme singe" ne peut pas.
Par conséquent, les programmeurs humains décomposent les mécanismes complexes en un ensemble d'éléments moins complexes et fonctionnels. Cependant, la décomposition peut se faire de différentes manières: dans de nombreuses voitures anciennes - le conduit sort par la porte et dans les voitures modernes - la défaillance de l'électronique de verrouillage empêche le moteur de démarrer, qui délivre pendant la réparation.
Ainsi, 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 devrait être basée sur le principe de séparation de la "responsabilité", c'est-à-dire selon les tâches des différents objets.
Revenons à l'alcool et aux avantages qu'une personne singe obtient lors de la décomposition:
- Le code est devenu extrêmement clair à tous les niveaux.
- Plusieurs programmeurs peuvent écrire du code à la fois (chacun écrit un élément distinct)
- Les tests automatisés sont simplifiés - plus l'élément est simple, plus il est facile de tester
- À partir de ces trois opérations, à l'avenir, vous pouvez additionner le glouton (en utilisant uniquement TakeBitOperation ), l'alcoolique (en utilisant uniquement DrinkUpOperation directement à partir de la bouteille) et satisfaire de nombreuses autres exigences commerciales.
Et, bien sûr, les inconvénients:
- Devra créer plus de types.
- Un buveur va boire pour la première fois quelques heures plus tard qu'il ne le pouvait
Définition 2. Variabilité unifiée.
Permettez à messieurs! Le cours de boisson remplit également une seule responsabilité - il boit! Et en général, le mot «responsabilité» est un concept extrêmement vague. Quelqu'un est responsable du sort de l'humanité, et quelqu'un est responsable d'élever les pingouins renversés au poteau.
Considérez deux implémentations de bingo. Le premier, mentionné ci-dessus, contient trois classes - verser, boire et manger un morceau.
La seconde est écrite à travers la méthodologie Forward et Only Forward et contient toute la logique de 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); } } }
Du point de vue d'un observateur extérieur, ces deux classes se ressemblent exactement et remplissent la seule responsabilité de «boire».
Embarras!
Ensuite, nous surfons sur Internet et découvrons une autre définition de la SRP - le principe de la variabilité uniforme.
Cette définition stipule que « le module a une et une seule raison de changement ». Autrement dit, «la responsabilité est une occasion de changement».
Maintenant, tout se met en place. Séparément, vous pouvez modifier les procédures de coulée, de boisson et de morsure, et dans l'alcool lui-même, nous ne pouvons changer que la séquence et la composition des opérations, par exemple, déplacer la collation avant de boire ou ajouter une lecture de pain grillé.
Dans l'approche Forward et Only Forward, tout ce qui peut être changé est changé uniquement dans la méthode Act . Il peut être lisible et efficace dans le cas où il y a peu de logique et il change rarement, mais il se termine souvent par des méthodes terribles de 500 lignes chacune, avec plus de si-nombres que requis pour l'entrée de la Russie dans l'OTAN.
Définition 3. Localisation des changements.
Les buveurs ne comprennent souvent pas pourquoi ils se sont réveillés dans l'appartement de quelqu'un d'autre, ni où se trouve leur téléphone portable. Il est temps d'ajouter une journalisation détaillée.
Commençons la journalisation avec le processus de coulée:
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}"); } }
En l'encapsulant dans PourOperation , nous avons agi sagement en termes de responsabilité et d'encapsulation, mais maintenant avec le principe de variabilité, nous sommes maintenant gênés. En plus de l'opération elle-même, qui peut changer, la journalisation elle-même devient variable. Nous devrons séparer et créer un enregistreur spécial pour l'opération de coulée:
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) } } }
Un lecteur méticuleux remarquera que LogAfter , LogBefore et OnError peuvent également être modifiés individuellement, et par analogie avec les étapes précédentes, il créera trois classes: PourLoggerBefore , PourLoggerAfter et PourErrorLogger .
Et en se souvenant qu'il y a trois opérations pour une frénésie - nous obtenons neuf classes de journalisation. En conséquence, l'alcool entier se compose de 14 (!!!) classes.
Hyperbole? À peine! Un homme singe avec une grenade de décomposition écrasera le «verseur» dans une carafe, un verre, des opérateurs de versage, un service d'approvisionnement en eau, un modèle physique d'une collision de 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 à ce stade que beaucoup en arrivent à la conclusion que les PÉR sont des contes des royaumes roses, et partent tordre les nouilles ...
... sans jamais connaître l'existence de la troisième définition de Srp:
" Les choses qui sont similaires au changement doivent être stockées en un seul endroit ." ou " Ce qui change ensemble doit être conservé au même endroit "
Autrement dit, si nous modifions la journalisation des opérations, nous devons la modifier au même endroit.
C'est un point très important - puisque toutes les explications SRP ci-dessus ont dit que les types devraient être séparés pendant qu'ils sont séparés, c'est-à-dire imposer une "restriction supérieure" à la taille de l'objet, et maintenant nous parlons d'une "limite inférieure" . En d'autres termes, la SRP nécessite non seulement "l'écrasement pendant l'écrasement", mais aussi de ne pas en faire trop - "n'écrasez pas les choses liées" . Ne vous compliquez pas inutilement. C'est la grande bataille du rasoir d'Occam avec l'homme singe!
Maintenant, l'alcool devrait être plus facile. En plus de ne pas diviser l'enregistreur IPourLogger en trois classes, nous pouvons également combiner tous les enregistreurs 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 le quatrième type d'opération nous est ajouté, la journalisation est prête pour cela. Et le code des opérations elles-mêmes est propre et exempt de bruit d'infrastructure.
En conséquence, nous avons 5 classes pour résoudre le problème d'alcool:
- Opération de coulée
- Opération de boisson
- Opération de bourrage
- Enregistreur
- Façade des Boolers
Chacun d'eux est strictement responsable d'une fonctionnalité, a une raison de changement. Toutes les règles similaires aux modifications se trouvent à proximité.
Exemples concrets
Sérialisation et désérialisationDans le cadre du développement du protocole de transfert de données, il est nécessaire de sérialiser et de désérialiser un certain type d '"utilisateur" en une chaîne.
User{ String Name; Int Age; }
Vous pourriez penser que la sérialisation et la désérialisation doivent être effectuées dans des classes distinctes:
UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} }
Puisque chacun d'eux a sa propre responsabilité et une raison de changement.
Mais ils ont une raison commune de changement - «changer le format de sérialisation des données».
Et lorsque vous changez ce format, la sérialisation et la désérialisation changeront toujours.
Selon le principe de localisation des changements, nous devons les combiner en une seule classe:
UserSerializer{ String deserialize(User){...} User serialize(String){...} }
Cela nous évite une complexité inutile et la nécessité de se rappeler que chaque fois que vous changez le sérialiseur, vous devez vous souvenir du désérialiseur.
Comptez et économisezVous devez calculer le revenu annuel de l'entreprise et l'enregistrer dans le fichier C: \ results.txt.
Nous résolvons rapidement cela avec une seule méthode:
void SaveGain(Company company){ // // }
Dès la définition de la tâche, il est clair qu'il existe deux sous-tâches - "Calculer les revenus" et "Enregistrer les revenus". Chacun d'eux a une raison de changement - «un changement dans la méthodologie de calcul» et «un changement dans le format d'épargne». Ces modifications ne se chevauchent pas. De plus, nous ne pouvons pas répondre de manière monosyllabique à la question - «que fait la méthode SaveGain?». Cette méthode ET calcule les revenus ET enregistre les résultats.
Par conséquent, vous devez diviser cette méthode en deux:
Gain CalcGain(Company company){..} void SaveGain(Gain gain){..}
Avantages:
- peut être testé séparément CalcGain
- localiser plus facilement les bugs et apporter des modifications
- lisibilité du code augmentée
- le risque d'erreur dans chacune des méthodes est réduit du fait de leur simplification
Logique métier sophistiquéeUne fois que nous avons écrit un service d'enregistrement automatique d'un client b2b. Et il y avait une méthode DIEU avec 200 lignes de contenu similaire:
- Accédez à 1C et créez un compte
- Avec ce compte, accédez au module de paiement et accédez-y
- Vérifier qu'un compte avec un tel compte n'a pas été créé sur le serveur principal
- Créez un nouveau compte
- Résultat de l'inscription dans le module de paiement et numéro 1c ajouter au service de résultats d'inscription
- Ajouter des informations de compte à ce tableau
- Créez un numéro de point pour ce client dans le service de points. Donnez ce numéro de compte de service 1s.
Il y avait environ 10 autres opérations commerciales avec une connectivité terrible sur cette liste. L'objet de compte était nécessaire à presque tout le monde. L'identifiant du point et le nom du client étaient nécessaires dans la moitié des appels.
Après une heure de refactoring, nous avons pu séparer le code d'infrastructure et certaines nuances de travail avec le compte en méthodes / classes distinctes. La méthode de Dieu est devenue plus facile, mais il restait 100 lignes de code qui ne voulaient pas être démêlées.
Quelques jours plus tard, il est apparu que l'essence de cette méthode "allégée" était l'algorithme commercial. Et que la description initiale des savoirs traditionnels était plutôt compliquée. Et c'est une tentative de briser cette méthode en morceaux qui constitueront une violation de la SRP, et non l'inverse.
Il est temps de laisser notre alcool tranquille. Essuyez les larmes - nous y reviendrons certainement d'une manière ou d'une autre. Maintenant, nous formalisons les connaissances de cet article.
- Séparez les éléments afin que chacun d'eux soit responsable d'une chose.
- La responsabilité signifie «cause de changement». Autrement dit, chaque élément n'a qu'une seule raison de changement, en termes de logique métier.
- Changements potentiels de la logique métier. doit être localisé. Les objets mutables doivent être proches.
Je n'ai pas satisfait aux critères suffisants pour la mise en œuvre du SRP. Mais il y a des conditions nécessaires:
1) Posez-vous une question - que fait cette classe / méthode / module / service. vous devez y répondre avec une définition simple. (merci à Brightori )
explicationsCependant, il est parfois très difficile de trouver une définition simple
2) La correction d'un bug ou l'ajout d'une nouvelle fonctionnalité affecte le nombre minimum de fichiers / classes. Idéalement, un.
explicationsÉtant donné que la responsabilité (pour une fonctionnalité ou un bogue) est encapsulée dans un seul fichier / classe, vous savez exactement où chercher et quoi modifier. Par exemple: pour modifier la sortie de la journalisation des opérations, il suffit de modifier uniquement l'enregistreur. Il n'est pas nécessaire de parcourir le reste du code.
Un autre exemple est l'ajout d'un nouveau contrôle d'interface utilisateur 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 «cassé».
3) Si plusieurs développeurs travaillent sur différentes fonctionnalités de votre projet, la probabilité d'un conflit de fusion, c'est-à-dire la probabilité que plusieurs développeurs changent le même fichier / classe en même temps, est minime.
explicationsSi lors de l'ajout d'une nouvelle opération "Verser de la vodka sous la table", vous devez toucher l'enregistreur, l'opération de boire et de verser - alors il semble que les responsabilités soient divisées de manière tordue. Bien sûr, ce n'est pas toujours possible, mais vous devez essayer de réduire ce chiffre.
4) Lorsque vous clarifiez une question sur la logique métier (d'un développeur ou d'un gestionnaire), vous montez strictement dans une classe / fichier et ne recevez des informations que de là.
explicationsLes fonctionnalités, les règles ou les algorithmes sont écrits de manière compacte en un seul endroit, et ne sont pas dispersés par des drapeaux dans l'espace de code.
5) La dénomination est claire.
explicationsNotre classe ou méthode est responsable d'une chose, et la responsabilité se reflète dans son nom.
AllManagersManagerService - très probablement, Dieu-classe
Paiement local - probablement pas
Au début de la conception, l'homme singe ne connaît pas et ne sent pas toutes les subtilités du problème résolu et peut donner une gaffe. Vous pouvez faire des erreurs de différentes manières:
- Faire des objets trop grands en collant différentes responsabilités
- Fractionner, diviser une seule responsabilité en plusieurs types différents
- Limites de responsabilité mal définies
Il est important de se souvenir de la règle: "il vaut mieux faire une grosse erreur" ou "pas sûr - ne pas se séparer". Si, par exemple, votre classe recueille deux responsabilités, cela reste compréhensible et peut être divisé en deux avec un changement minimal du code client. La collecte d'un verre à partir de fragments de verre est généralement plus difficile en raison du contexte réparti sur plusieurs fichiers et du manque de dépendances nécessaires dans le code client.
Il est temps d'arrondir
La portée de SRP n'est pas limitée à OOP et SOLID. Elle s'applique aux méthodes, fonctions, classes, modules, microservices et services. Il s'applique à la fois au développement «figax-figax-and-in-prod» et «rocket-sainz», rendant le monde un peu meilleur partout. Si vous y réfléchissez, c'est presque le principe fondamental de toute ingénierie. L'ingénierie mécanique, les systèmes de contrôle et, en fait, tous les systèmes complexes sont construits à partir de composants, et la «fragmentation incomplète» prive les concepteurs de flexibilité, de «fragmentation» - d'efficacité et de limites incorrectes - de raison et de tranquillité d'esprit.

Le SRP n'est pas inventé par la nature et ne fait pas partie de la science exacte. Il sort de nos limites biologiques et psychologiques, ce n'est qu'un moyen de contrôler et de développer des systèmes complexes en utilisant le cerveau d'un singe humain. Il nous explique comment décomposer le système. Le libellé original nécessitait une bonne dose de télépathie, mais j'espère que cet article a légèrement dissipé l'écran de fumée.