Principe de responsabilité unique. Pas aussi simple qu'il y paraît

Principe de responsabilité unique. Pas aussi simple qu'il y paraît Principe de responsabilité unique, également appelé principe de responsabilité unique,
c'est-à-dire le principe de variabilité uniforme - un gars extrêmement glissant à comprendre et une question si nerveuse lors d'un entretien avec un programmeur.

Ma première connaissance sérieuse de ce principe a eu lieu 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 des larves des étudiants - de vrais étudiants.

Dans la forêt, nous avons été divisés en groupes de 8 à 9 personnes chacun et avons organisé un concours : quel groupe boirait une bouteille de vodka le plus rapidement, à condition que la première personne du groupe verse de la vodka dans un verre, la seconde la boive, et le troisième prend une collation. L’unité qui a terminé son opération se place en fin de file d’attente du groupe.

Le cas où la taille de la file d’attente était un multiple de trois constituait une bonne implémentation de SRP.

Définition 1. Responsabilité unique.

La définition officielle du principe de responsabilité unique (SRP) stipule que chaque entité a sa propre responsabilité et sa propre raison d'existence, et qu'elle n'a qu'une seule responsabilité.

Considérons l'objet « Buveur » (Culbuteur).
Pour mettre en œuvre le principe SRP, nous diviserons les responsabilités en trois :

  • On verse (PourOpération)
  • On boit (Opération DrinkUp)
  • On prend une collation (Opération TakeBite)

Chacun des participants au processus est responsable d'un élément du processus, c'est-à-dire qu'il a une responsabilité atomique : boire, verser ou grignoter.

L’abreuvoir, quant à lui, est une façade pour ces opérations :

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

Principe de responsabilité unique. Pas aussi simple qu'il y paraît

Pourquoi?

Le programmeur humain écrit du code pour l'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 d’un ivrogne, il existe trois de ces termes. Cependant, si nous écrivons le code sur une seule feuille, elle contiendra alors des mains, des lunettes, des bagarres et des arguments politiques sans fin. Et tout cela sera dans le corps d’une seule méthode. Je suis sûr que vous avez vu un tel code dans votre pratique. Ce n'est pas le test le plus humain pour le psychisme.

D’un autre côté, l’homme singe est conçu pour simuler des objets du monde réel dans sa tête. Dans son imagination, il peut les assembler, en assembler de nouveaux objets et les démonter de la même manière. Imaginez un vieux modèle de voiture. Dans votre imagination, vous pouvez ouvrir la porte, dévisser la garniture de porte et y voir les mécanismes de lève-vitre, à l'intérieur desquels se trouveront des engrenages. Mais on ne peut pas voir tous les composants de la machine en même temps, dans un seul « listing ». Du moins, « l’homme-singe » ne le 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, il peut se décomposer de différentes manières : dans de nombreuses voitures anciennes, le conduit d'air pénètre dans la porte, et dans les voitures modernes, une défaillance de l'électronique de serrure empêche le démarrage du moteur, ce qui peut poser problème lors des réparations.

Et donc, Le SRP est un principe qui explique COMMENT décomposer, c'est-à-dire où tracer la ligne de démarcation.

Il dit qu'il faut décomposer selon le principe de partage de la « responsabilité », c'est-à-dire selon les tâches de certains objets.

Principe de responsabilité unique. Pas aussi simple qu'il y paraît

Revenons à la boisson et aux avantages dont bénéficie l'homme singe lors de la décomposition :

  • Le code est devenu extrêmement clair à tous les niveaux
  • Le code peut être écrit par plusieurs programmeurs à 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 à tester
  • La compositionnalité du code apparaît - vous pouvez remplacer Opération DrinkUp à une opération dans laquelle un ivrogne verse du liquide sous la table. Ou remplacez l'opération de versement par une opération dans laquelle vous mélangez du vin et de l'eau ou de la vodka et de la bière. En fonction des besoins métiers, vous pouvez tout faire sans toucher au code de la méthode Tippler.Act.
  • A partir de ces opérations vous pouvez plier le glouton (en utilisant uniquement TakeBitOpération), alcoolique (en utilisant uniquement Opération DrinkUp directement de la bouteille) et répondent à de nombreuses autres exigences commerciales.

(Oh, il semble que ce soit déjà un principe OCP, et j'ai violé la responsabilité de ce message)

Et bien sûr, les inconvénients :

  • Nous devrons créer plus de types.
  • Un ivrogne boit pour la première fois quelques heures plus tard qu'il ne l'aurait fait autrement.

Définition 2. Variabilité unifiée.

Permettez-moi, messieurs ! La classe de boisson a également une seule responsabilité : elle 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 de l'élevage des pingouins renversés au 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 est écrite selon la méthodologie « Forward and Only Forward » et contient toute la logique de la méthode Agis:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
с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 partagent la même responsabilité de « boire ».

Confusion!

Ensuite, nous allons en ligne et découvrons une autre définition du SRP – le principe de variabilité unique.

SCP déclare que "Un module a une et une seule raison de changer". Autrement dit, « la responsabilité est une raison de changement ».

(Il semble que les gars qui ont proposé la définition originale avaient confiance dans les capacités télépathiques de l'homme-singe)

Maintenant, tout se met en place. Séparément, nous pouvons modifier les procédures de versement, de consommation et de collation, mais chez le buveur lui-même, nous ne pouvons modifier que la séquence et la composition des opérations, par exemple en déplaçant la collation avant de la boire ou en ajoutant la lecture d'un toast.

Dans l'approche « Forward and Only Forward », tout ce qui peut être modifié n'est modifié que dans la méthode Agis. Cela peut être lisible et efficace lorsqu’il y a peu de logique et cela change rarement, mais cela aboutit souvent à de terribles méthodes de 500 lignes chacune, avec plus de déclarations « si » que ce qui est nécessaire pour que la Russie rejoigne 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 par enregistrer 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 PourOpération, nous avons agi avec sagesse du point de vue de la responsabilité et de l'encapsulation, mais maintenant nous nous confondons avec le principe de variabilité. Outre le fonctionnement lui-même, qui peut changer, la journalisation elle-même devient également modifiable. Vous devrez 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)
        }
    }
}

Le lecteur méticuleux remarquera que JournalAprès, Se connecterAvant и OnError peut également être modifié individuellement et, par analogie avec les étapes précédentes, créera trois classes : PourLoggerAvant, PourLoggerAfter и PourErrorLogger.

Et en nous rappelant qu'il y a trois opérations pour un buveur, nous obtenons neuf classes d'exploitation forestière. En conséquence, l'ensemble du cercle de boisson se compose de 14 (!!!) classes.

Hyperbole? À peine! Un homme-singe avec une grenade à décomposition divisera le « verseur » en une carafe, un verre, des opérateurs de versage, un service d'approvisionnement en eau, un modèle physique de collision de molécules, et pendant le trimestre suivant, il tentera de démêler les dépendances sans variables globales. Et croyez-moi, il ne s'arrêtera pas.

C'est à ce moment-là que beaucoup en viennent à la conclusion que les SRP sont des contes de fées des royaumes roses, et s'en vont jouer aux nouilles...

...sans jamais connaître l'existence d'une troisième définition de Srp :

« Le principe de responsabilité unique stipule que les choses qui ressemblent à du changement doivent être stockées au même endroit". ou "Quels changements ensemble doivent être conservés au même endroit »

Autrement dit, si nous modifions la journalisation d’une opération, nous devons la modifier au même endroit.

C'est un point très important - puisque toutes les explications du SRP ci-dessus disaient qu'il fallait écraser les types pendant qu'ils étaient écrasés, c'est-à-dire qu'elles imposaient une « limite supérieure » à la taille de l'objet, et maintenant nous parlons déjà d'une « limite inférieure » . Autrement dit, Le SRP exige non seulement « d’écraser en écrasant », mais aussi de ne pas en faire trop – « n’écrasez pas les objets imbriqués ». C'est la grande bataille entre le rasoir d'Occam et l'homme singe !

Principe de responsabilité unique. Pas aussi simple qu'il y paraît

Maintenant, le buveur devrait se sentir mieux. Outre le fait qu'il n'est pas nécessaire de diviser le logger IPourLogger en trois classes, nous pouvons également combiner tous les loggers 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 nous ajoutons un quatrième type d'opération, alors la journalisation correspondante est déjà prête. Et le code des opérations lui-même est propre et exempt de bruit d’infrastructure.

En conséquence, nous avons 5 cours pour résoudre le problème de la consommation d'alcool :

  • Opération de coulée
  • Opération de boisson
  • Opération de brouillage
  • Enregistreur
  • Façade de buveur

Chacun d’eux est strictement responsable d’une fonctionnalité et a une seule raison de changement. Toutes les règles similaires au changement se trouvent à proximité.

Exemple concret

Nous avons déjà écrit un service pour enregistrer automatiquement un client b2b. Et une méthode DIEU est apparue pour 200 lignes de contenu similaire :

  • Allez sur 1C et créez un compte
  • Avec ce compte, rendez-vous dans le module de paiement et créez-le là
  • Vérifiez qu'un compte avec un tel compte n'a pas été créé sur le serveur principal
  • Créer un nouveau compte
  • Ajoutez les résultats d'inscription dans le module de paiement et le numéro 1c au service des résultats d'inscription
  • Ajouter les informations du compte à ce tableau
  • Créez un numéro de point pour ce client dans le service point. Transmettez votre numéro de compte 1c à ce service.

Et il y avait environ 10 autres opérations commerciales sur cette liste avec une connectivité épouvantable. Presque tout le monde avait besoin de l’objet compte. L'ID 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 de l'infrastructure et certaines nuances liées au travail avec un compte en méthodes/classes distinctes. La méthode Dieu a rendu les choses plus faciles, mais il restait 100 lignes de code qui ne voulaient tout simplement pas être démêlées.

Ce n’est qu’au bout de quelques jours qu’il est devenu clair que l’essence de cette méthode « légère » est un algorithme métier. Et que la description originale des spécifications techniques était assez complexe. Et c’est la tentative de briser cette méthode en morceaux qui violera le SRP, et non l’inverse.

Formalisme.

Il est temps de laisser notre ivrogne tranquille. Séchez vos larmes - nous y reviendrons certainement un jour. Formalisons maintenant les connaissances issues de cet article.

Formalisme 1. Définition du SRP

  1. Séparez les éléments pour que chacun d'eux soit responsable d'une chose.
  2. La responsabilité signifie « raison de changer ». Autrement dit, chaque élément n’a qu’une seule raison de changement, en termes de logique métier.
  3. Modifications potentielles de la logique métier. doit être localisé. Les éléments qui changent de manière synchrone doivent être à proximité.

Formalisme 2. Critères d'auto-test nécessaires.

Je n'ai pas vu de critères suffisants pour remplir le SRP. Mais il y a des conditions nécessaires :

1) Demandez-vous ce que fait cette classe/méthode/module/service. vous devez y répondre avec une définition simple. ( Merci Brightori )

explications

Cependant, 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 un nombre minimum de fichiers/classes. Idéalement - un.

explications

Étant donné que la responsabilité (d'une fonctionnalité ou d'un bogue) est encapsulée dans un seul fichier/classe, vous savez exactement où chercher et quoi modifier. Par exemple : la fonctionnalité de modification du résultat des opérations de journalisation nécessitera de modifier uniquement l'enregistreur. Il n’est pas nécessaire de parcourir le reste du code.

Un autre exemple consiste à ajouter 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 en faites trop.

3) Si plusieurs développeurs travaillent sur différentes fonctionnalités de votre projet, alors la probabilité d'un 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 minime.

explications

Si, lors de l'ajout d'une nouvelle opération « Verser la vodka sous la table », vous devez affecter l'enregistreur, l'opération de boire et de verser, alors il semble que les responsabilités soient divisées de travers. Bien entendu, cela n’est pas toujours possible, mais nous devrions essayer de réduire ce chiffre.

4) Lorsqu'on vous pose une question de clarification sur la logique métier (de la part d'un développeur ou d'un gestionnaire), vous accédez strictement à une classe/fichier et recevez des informations uniquement à partir de là.

explications

Les fonctionnalités, règles ou algorithmes sont écrits de manière compacte, chacun au même endroit, et non dispersés avec des indicateurs dans tout l'espace de code.

5) Le nom est clair.

explications

Notre classe ou méthode est responsable d'une chose, et la responsabilité se reflète dans son nom

AllManagersManagerService - probablement une classe Dieu
LocalPayment - probablement pas

Formalisme 3. Méthodologie de développement Occam-first.

Au début de la conception, l'homme singe ne connaît pas et ne ressent pas toutes les subtilités du problème à résoudre et peut se tromper. Vous pouvez faire des erreurs de différentes manières :

  • Rendre les objets trop volumineux en fusionnant différentes responsabilités
  • Recadrage en divisant une seule responsabilité en plusieurs types différents
  • Définir incorrectement les limites de la responsabilité

Il est important de rappeler la règle : « il vaut mieux faire une grosse erreur » ou « si vous n’êtes pas sûr, ne partagez pas ». Si, par exemple, votre classe contient deux responsabilités, elle reste alors compréhensible et peut être divisée en deux avec des modifications minimes du code client. Assembler un verre à partir d'éclats de verre est généralement plus difficile en raison de la répartition du contexte sur plusieurs fichiers et du manque de dépendances nécessaires dans le code client.

Il est temps d'arrêter ça

La portée de SRP ne se limite pas à la POO et à SOLID. Il s'applique aux méthodes, fonctions, classes, modules, microservices et services. Cela s’applique à la fois au développement « figax-figax-and-prod » et « rocket-science », rendant le monde un peu meilleur partout. Si vous y réfléchissez bien, 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 « sous-fragmentation » prive les concepteurs de flexibilité, la « surfragmentation » prive les concepteurs d'efficacité et des limites incorrectes les privent de raison et de tranquillité d'esprit.

Principe de responsabilité unique. Pas aussi simple qu'il y paraît

La SRP n’est pas inventée par la nature et ne fait pas partie de la science exacte. Cela brise nos limites biologiques et psychologiques et n'est qu'un moyen de contrôler et de développer des systèmes complexes à l'aide du cerveau de l'homme-singe. Il nous explique comment décomposer un système. La formulation originale nécessitait pas mal de télépathie, mais j’espère que cet article dissipera une partie de l’écran de fumée.

Source: habr.com

Ajouter un commentaire