Clarifier le principe de responsabilité unique

64

Le principe de responsabilité unique stipule qu'une classe doit faire une et une seule chose. Certains cas sont assez clairs. D'autres, cependant, sont difficiles car ce qui ressemble à «une chose» vu à un niveau d'abstraction donné peut être multiple si on le regarde à un niveau inférieur. Je crains également que, si le principe de responsabilité unique soit respecté aux niveaux inférieurs, il en résulte un code de raviole verbeux excessivement découplé, où plus de lignes sont consacrées à la création de petites classes pour tout et à la plomberie d’informations plutôt qu’à la résolution du problème actuel.

Comment décririez-vous ce que "une chose" signifie? Quels sont certains signes concrets qu'une classe fait plus que "une chose"?

Dsimcha
la source
6
+1 pour le code "ravioli". Au début de ma carrière, j'étais l'une de ces personnes qui l'avaient poussée trop loin. Pas seulement avec les classes, mais aussi avec la modularisation des méthodes. Mon code était parsemé de tonnes de petites méthodes qui faisaient quelque chose de simple, juste pour diviser un problème en petits morceaux qui pourraient tenir sur l'écran sans défilement. Évidemment, c'était souvent aller trop loin.
Bobby Tables

Réponses:

50

J'aime beaucoup la manière dont Robert C. Martin (Oncle Bob) réaffirme le principe de responsabilité unique (lié au PDF) :

Il ne devrait jamais y avoir plus d'une raison pour qu'une classe change

C'est une définition légèrement différente de la définition traditionnelle du «ne devrait faire qu'une chose» et j'aime bien cela parce que cela vous oblige à changer votre façon de penser de votre classe. Au lieu de penser à "est-ce que cela fait une chose?", Vous pensez plutôt à ce qui peut changer et à la manière dont ces changements affectent votre classe. Ainsi, par exemple, si la base de données change, votre classe doit-elle changer? Qu'en est-il si le périphérique de sortie change (par exemple, un écran, un périphérique mobile ou une imprimante)? Si votre classe doit changer en raison de changements venant de nombreuses autres directions, c'est un signe que votre classe a trop de responsabilités.

Dans l'article lié, Oncle Bob conclut:

Le PÉR est l’un des principes les plus simples et des plus difficiles à comprendre. Les responsabilités communes sont quelque chose que nous faisons naturellement. Trouver et séparer ces responsabilités les unes des autres est une grande partie de la conception du logiciel.

Paddyslacker
la source
2
J'aime la façon dont il le dit aussi, il semble plus facile de vérifier quand il s'agit de changement plutôt que de la "responsabilité" abstraite.
Matthieu M.
C'est en fait un excellent moyen de le dire. J'aime ça. En guise de remarque, j’ai tendance à penser que le PÉR s’applique beaucoup plus fortement aux méthodes. Parfois, une classe doit simplement faire deux choses (peut-être que la classe relie deux domaines), mais une méthode ne devrait presque jamais faire plus que ce que vous pouvez décrire succinctement par sa signature de type.
CodexArcanum
1
Je viens de le montrer à mon diplômé - bonne lecture et bon rappel à moi-même.
Martijn Verburg
1
Ok, cela a beaucoup de sens lorsque vous combinez cela avec l’idée que le code non sur-conçu doit uniquement planifier les changements susceptibles de se produire dans un avenir prévisible, et non tous les changements possibles. Je réaffirmerais donc cela légèrement, car "il ne devrait y avoir qu'une seule raison pour laquelle une classe doit changer qui est susceptible de se produire dans un avenir prévisible". Cela encourage à privilégier la simplicité dans les parties de la conception qui sont peu susceptibles de changer et le découplage dans les parties susceptibles de changer.
Dsimcha
18

Je n'arrête pas de me demander quel est le problème que SRP tente de résoudre. Quand SRP m'aide-t-il? Voici ce que je suis venu avec:

Vous devez repenser la responsabilité / fonctionnalité en dehors de la classe lorsque:

1) Vous avez dupliqué la fonctionnalité (DRY)

2) Vous trouvez que votre code a besoin d'un autre niveau d'abstraction pour vous aider à le comprendre (KISS)

3) Vous constatez que vos experts en matière de domaine considèrent que les fonctionnalités sont un composant distinct (le langage omniprésent)

Vous NE DEVRIEZ PAS repenser la responsabilité d'une classe lorsque:

1) Il n'y a pas de fonctionnalité dupliquée.

2) La fonctionnalité n'a pas de sens en dehors du contexte de votre classe. En d'autres termes, votre classe fournit un contexte dans lequel il est plus facile de comprendre la fonctionnalité.

3) Les experts de votre domaine n’ont aucune idée de cette responsabilité.

Je me rends compte que si SRP s’applique au sens large, nous négocions un type de complexité (essayer de faire taire une classe avec beaucoup trop de choses à l’intérieur) avec un autre type (essayer de garder tous les collaborateurs / niveaux de l’abstraction pour comprendre ce que font réellement ces classes).

En cas de doute, laissez tomber! Vous pouvez toujours refacturer plus tard, quand il y a un cas clair pour cela.

Qu'est-ce que tu penses?

Brandon
la source
Bien que ces directives puissent être utiles ou non, elles n’ont vraiment rien à voir avec le PRS défini dans SOLID, n’est-ce pas?
Sara
Merci. Je n'arrive pas à croire à la folie inhérente aux soi-disant principes SOLID, qui rendent un code très simple cent fois plus complexe sans raison valable . Les points que vous donnez décrivent des raisons réelles d'implémenter SRP. Je pense que les principes que vous avez donnés ci-dessus devraient devenir leur propre acronyme, et jeter "SOLID", cela fait plus de mal que de bien. "Architecture Astronauts" en effet, comme vous l'avez souligné dans le fil conducteur qui me mène ici.
Nicholas Petersen
4

Je ne sais pas s'il y a une échelle objective, mais ce qui donnerait cela serait les méthodes - pas tellement leur nombre, mais la variété de leurs fonctions. Je conviens que vous pouvez aller trop loin dans la décomposition mais je ne suivrais aucune règle stricte à ce sujet.

Jeremy
la source
4

La réponse réside dans la définition

Ce que vous définissez comme étant la responsabilité, vous donne finalement la limite.

Exemple:

J'ai un composant qui a la responsabilité d' afficher les factures -> dans ce cas, si je commençais à ajouter quelque chose de plus, je briserais le principe.

Si par contre si je disais la responsabilité de traiter les factures -> en ajoutant plusieurs fonctions plus petites (par exemple, impression de facture , mise à jour de facture ), elles se situent toutes dans cette limite.

Toutefois, si ce module commençait à gérer une fonctionnalité en dehors des factures , il se trouverait en dehors de cette limite.

Nuit noire
la source
Je suppose que le principal problème ici est le mot "manipulation". C'est trop générique pour définir une seule responsabilité. Je pense qu’il serait préférable d’avoir un composant responsable de la facture imprimée et un autre composant pour la facture de mise à jour plutôt qu’un seul composant: gérer {imprimer, mettre à jour et - pourquoi pas? - afficher, quelle que soit la facture} .
Machado
1
Le PO demande essentiellement "Comment définissez-vous une responsabilité?" Ainsi, lorsque vous dites que la responsabilité revient à ce que vous définissez, vous ne faites que répéter la question.
Despertar
2

Je le vois toujours à deux niveaux:

  • Je m'assure que mes méthodes ne font qu'une chose et le font bien
  • Je vois une classe comme un regroupement logique (OO) de ces méthodes qui représente bien une chose

Donc, quelque chose comme un objet de domaine appelé Dog:

Dogest ma classe mais les chiens peuvent faire beaucoup de choses! Je pourrais avoir des méthodes telles que walk(), run()et bite(DotNetDeveloper spawnOfBill)(désolé , je n'ai pas pu résister; p).

Si cela Dogdevient trop compliqué, je penserais à la façon dont des groupes de ces méthodes peuvent être modélisés ensemble dans une autre classe, telle qu'une Movementclasse, qui pourrait contenir my walk()et run()méthodes.

Il n'y a pas de règle absolue, votre conception OO évoluera avec le temps. J'essaie d'avoir une interface publique / interface claire et des méthodes simples qui font bien une chose et une chose.

Martijn Verburg
la source
Bite devrait vraiment prendre une instance de Object, et un DotNetDeveloper devrait être une sous-classe de Person (généralement, en tout cas!)
Alan Pearce
@Alan - Il a corrigé cela pour vous :-)
Martijn Verburg Le
1

Je le regarde plus dans les lignes d'une classe ne devrait représenter qu'une chose. Pour m'approprier l'exemple de Karianna, j'ai ma Dogclasse, qui a des méthodes pour walk(), run()et bark(). Je ne vais pas ajouter des méthodes pour meaow(), squeak(), slither()ou fly()parce que ce ne sont pas des choses que les chiens. Ce sont des choses que font d'autres animaux, et ces autres animaux auraient leurs propres classes pour les représenter.

(BTW, si votre chien ne voler, alors vous devriez probablement arrêter de le jeter hors de la fenêtre).

JohnL
la source
+1 pour "si votre chien vole, alors vous devriez probablement arrêter de le jeter par la fenêtre". :)
Bobby Tables
Au-delà de la question de ce que la classe devrait représenter, que représente une instance ? Si on considère SeesFoodcomme une caractéristique de DogEyes, Barkcomme chose faite par un DogVoice, et Eatcomme une chose faite par un DogMouth, alors la logique if (dog.SeesFood) dog.Eat(); else dog.Bark();devient if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, perdant tout sens de l'identité que les yeux, la bouche et la voix sont tous connectés à une seule entité.
Supercat
@supercat c'est un point juste, même si le contexte est important. Si le code que vous mentionnez est dans la Dogclasse, alors il est probablement Doglié. Sinon, vous vous retrouveriez probablement avec quelque chose comme myDog.Eyes.SeesFoodplutôt que juste eyes.SeesFood. Une autre possibilité est que Dogexpose l' ISeeinterface qui exige la Dog.Eyespropriété et la SeesFoodméthode.
JohnL
@JohnL: Si les mécanismes réels de la vision sont gérés par les yeux d'un chien, essentiellement de la même manière que ceux d'un chat ou d'un zèbre, il peut alors être logique de faire en sorte que les mécanismes soient gérés par une Eyeclasse, mais un chien doit "voir" en utilisant ses yeux, plutôt que de simplement avoir des yeux qui peuvent voir. Un chien n'est pas un œil, mais ce n'est pas simplement un porte-vue. C'est une "chose qui peut [au moins essayer de] voir", et devrait être décrite comme telle par l'interface. Même un chien aveugle peut être demandé s'il voit de la nourriture; cela ne sera pas très utile, car le chien dira toujours "non", mais demander n'est pas mal.
Supercat
Ensuite, vous utiliseriez l'interface ISee comme je le décris dans mon commentaire.
JohnL
1

Une classe devrait faire une chose lorsqu'elle est considérée à son propre niveau d'abstraction. Il fera sans doute beaucoup de choses à un niveau moins abstrait. C'est ainsi que les classes travaillent pour rendre les programmes plus faciles à maintenir: elles cachent les détails de l'implémentation si vous n'avez pas besoin de les examiner de près.

J'utilise les noms de classe comme test pour cela. Si je ne peux pas donner à une classe un nom descriptif assez court, ou si ce nom contient un mot comme "Et", je viole probablement le principe de responsabilité unique.

D'après mon expérience, il est plus facile de maintenir ce principe aux niveaux inférieurs, où les choses sont plus concrètes.

David Thornley
la source
0

Il s'agit d'avoir un rôle unique .

Chaque classe doit être reprise par un nom de rôle. Un rôle est en fait un (ensemble de) verbe (s) associé (s) à un contexte.

Par exemple :

Fichier fournit un accès au fichier. FileManager gère les objets File.

La ressource contient des données pour une ressource d'un fichier. ResourceManager contient et fournit toutes les ressources.

Ici, vous pouvez voir que certains verbes comme "gérer" impliquent un ensemble d'autres verbes. Les verbes seuls sont mieux considérés comme des fonctions que des classes, la plupart du temps. Si le verbe implique trop d'actions ayant leur propre contexte commun, il devrait alors être une classe en soi.

Donc, l’idée est seulement de vous donner une idée simple de ce que fait la classe en définissant un rôle unique, qui pourrait être l’agrégat de plusieurs sous-rôles (joués par des objets membres ou d’autres objets).

Je construis souvent des classes de gestionnaires qui contiennent plusieurs autres classes. Comme une usine, un registre, etc. Voir une classe de gestionnaires comme une sorte de chef de groupe, un chef d'orchestre qui guide les autres peuples à travailler ensemble pour atteindre une idée de haut niveau. Il a un rôle, mais implique de travailler avec d'autres rôles uniques à l'intérieur. Vous pouvez également voir cela de la manière dont une entreprise est organisée: un PDG n’est pas productif sur le plan de la productivité pure, mais s’il n’y est pas, rien ne peut fonctionner correctement ensemble. C'est son rôle.

Lorsque vous concevez, identifiez des rôles uniques. Et pour chaque rôle, voyez à nouveau s'il ne peut pas être coupé dans plusieurs autres rôles. De cette façon, si vous avez besoin de changer simplement la façon dont votre gestionnaire construit les objets, il vous suffit de changer de fabrique et d’avoir la tranquillité à l’esprit.

Klaim
la source
-1

Le SRP ne consiste pas uniquement à diviser les classes, mais également à déléguer des fonctionnalités.

Dans l'exemple de chien utilisé ci-dessus, n'utilisez pas SRP comme justification pour avoir 3 classes distinctes telles que DogBarker, DogWalker, etc. (faible cohésion). Au lieu de cela, regardez l'implémentation des méthodes d'une classe et décidez si elles "en savent trop". Vous pouvez toujours avoir dog.walk (), mais la méthode walk () devrait probablement déléguer à une autre classe les détails de la marche à suivre.

En fait, nous permettons à la classe Dog d’avoir une raison de changer: parce que les chiens changent. Bien sûr, en combinant cela avec d'autres principes SOLID, vous étendez Extend Dog pour de nouvelles fonctionnalités plutôt que de changer Dog (ouvert / fermé). Et vous injecteriez vos dépendances telles que IMove et IEat. Et bien sûr, vous feriez ces interfaces séparées (séparation des interfaces). Chien ne changerait que si nous trouvions un bug ou si les chiens étaient fondamentalement modifiés (Liskov Sub, ne prolongez pas et supprimez le comportement).

SOLID a pour effet net d’écrire un nouveau code plus souvent que de modifier le code existant, ce qui est une grande victoire.

utilisateur1488779
la source
-1

Tout dépend de la définition de responsabilité et de l'impact de cette définition sur la maintenance de votre code. Tout se résume à une chose et c'est ainsi que votre conception va vous aider à gérer votre code.

Et comme quelqu'un l'a dit, "marcher sur l'eau et concevoir un logiciel à partir de spécifications données est facile, à condition que les deux soient gelés".

Donc, si nous définissons la responsabilité de manière plus concrète, nous n’avons pas besoin de la changer.

Parfois, les responsabilités sont évidentes, mais parfois, elles peuvent être subtiles et nous devons prendre des décisions judicieuses.

Supposons que nous ajoutions une autre responsabilité à la classe Dog nommée catchThief (). Maintenant, cela pourrait mener à une responsabilité supplémentaire supplémentaire. Demain, si le département de la police doit changer le comportement du chien qui attrape le voleur, il devra changer de classe de chien. Dans ce cas, il serait préférable de créer une autre sous-classe et nommez-la ThiefCathcerDog. Mais dans un autre point de vue, si nous sommes certains que cela ne changera en aucun cas, ou que la manière dont catchThief a été mis en œuvre dépend de certains paramètres externes, il est parfaitement correct d'avoir cette responsabilité. Si les responsabilités ne sont pas étonnamment étranges, nous devons le décider judicieusement en fonction du cas d'utilisation.

AKS
la source
-1

"Une raison de changer" dépend de qui utilise le système. Assurez-vous de disposer des cas d'utilisation pour chaque acteur et dressez une liste des modifications les plus probables. Pour chaque changement de cas d'utilisation possible, assurez-vous qu'une seule classe serait affectée par ce changement. Si vous ajoutez un cas d'utilisation entièrement nouveau, assurez-vous que vous n'aurez besoin que d'étendre une classe pour le faire.

kiwicomb123
la source
1
Une des raisons de changer n'a rien à voir avec le nombre de cas d'utilisation, d'acteurs ou la probabilité d'un changement. Vous ne devriez pas faire une liste de changements probables. Il est vrai qu'un changement ne devrait affecter qu'une classe. Pouvoir étendre une classe pour s'adapter à ce changement est une bonne chose, mais c'est le principe ouvert-fermé, pas le PÉR.
candied_orange
Pourquoi ne devrions-nous pas dresser une liste des changements les plus probables? Conception spéculative?
kiwicomb123
parce que cela n’aidera pas, ne sera pas complet et qu’il existe des moyens plus efficaces de faire face au changement que de tenter de le prédire. Attends-toi. Isoler les décisions et l'impact sera minime.
candied_orange
D’accord, je comprends, cela viole le principe «tu n’en as pas besoin».
kiwicomb123
En fait, yagni peut être poussé aussi loin. C'est vraiment pour vous empêcher de mettre en œuvre des cas d'utilisation spéculatifs. Ne pas vous empêcher d'isoler un cas d'utilisation implémenté.
candied_orange