Le principe de responsabilité unique peut-il / devrait-il être appliqué au nouveau code?

20

Le principe est défini comme des modules ayant une raison de changer . Ma question est, ces raisons de changer ne sont sûrement pas connues jusqu'à ce que le code commence réellement à changer ?? Presque chaque morceau de code a de nombreuses raisons pour lesquelles il pourrait éventuellement changer, mais essayer sûrement d'anticiper tout cela et de concevoir votre code dans cet esprit se retrouverait avec un code très pauvre. N'est-ce pas une meilleure idée de ne vraiment commencer à appliquer SRP que lorsque des demandes de changement de code commencent à arriver? Plus précisément, lorsqu'un morceau de code a changé plus d'une fois pour plus d'une raison, prouvant ainsi qu'il a plus d'une raison de changer. Cela semble très anti-Agile de tenter de deviner les raisons du changement.

Un exemple serait un morceau de code qui imprime un document. Une demande vient le modifier pour imprimer au format PDF, puis une deuxième demande est faite pour le modifier afin d'appliquer une mise en forme différente au document. À ce stade, vous avez la preuve de plus d'une seule raison de changer (et d'une violation de SRP) et devez effectuer le refactoring approprié.

SeeNoWeevil
la source
6
@Frank - il est en fait généralement défini comme ça - voir par exemple en.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans
1
La façon dont vous le formulez n'est pas la façon dont je comprends la définition de SRP.
Pieter B
2
Chaque ligne de code a (au moins) deux raisons d'être modifiée: elle contribue à un bogue ou elle interfère avec une nouvelle exigence.
Bart van Ingen Schenau
1
@BartvanIngenSchenau: LOL ;-) Si vous le voyez de cette façon, le SRP ne peut être appliqué nulle part.
Doc Brown
1
@DocBrown: vous pouvez le faire si vous ne couplez pas SRP avec la modification du code source. Par exemple, si vous interprétez SRP comme étant capable de donner un compte rendu complet de ce que fait une classe / fonction dans une phrase sans utiliser le mot et (et pas de formulation de belette pour contourner cette restriction).
Bart van Ingen Schenau

Réponses:

27

Bien sûr, le principe YAGNI vous dira d'appliquer SRP pas avant d'en avoir vraiment besoin. Mais la question que vous devez vous poser est: dois-je d'abord appliquer SRP et uniquement lorsque je dois réellement changer mon code?

D'après mon expérience, l'application de SRP vous offre un avantage beaucoup plus tôt: quand vous devez savoir et comment appliquer un changement spécifique dans votre code. Pour cette tâche, vous devez lire et comprendre vos fonctions et classes existantes. Cela devient beaucoup plus facile lorsque toutes vos fonctions et classes ont une responsabilité spécifique. Donc, à mon humble avis, vous devez appliquer SRP chaque fois qu'il rend votre code plus facile à lire, chaque fois qu'il rend vos fonctions plus petites et plus auto-descriptives. La réponse est donc oui , il est logique d'appliquer SRP même pour le nouveau code.

Par exemple, lorsque votre code d'impression lit un document, formate le document et imprime le résultat sur un périphérique spécifique, ce sont 3 responsabilités clairement séparables. Faites-en au moins 3 fonctions, donnez-leur des noms. Par exemple:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

Maintenant, lorsque vous obtenez une nouvelle exigence pour modifier la mise en forme du document ou une autre pour imprimer au format PDF, vous savez exactement à laquelle de ces fonctions ou de ces emplacements dans le code vous devez appliquer les modifications, et plus important encore, dans le cas contraire.

Donc, chaque fois que vous arrivez à une fonction, vous ne comprenez pas parce que la fonction en fait "trop", et vous ne savez pas si et où appliquer un changement, alors envisagez de refactoriser la fonction en fonctions distinctes et plus petites. N'attendez pas d'avoir à changer quelque chose. Le code est 10 fois plus lu que modifié et les fonctions plus petites sont beaucoup plus faciles à lire. D'après mon expérience, lorsqu'une fonction a une certaine complexité, vous pouvez toujours diviser la fonction en différentes responsabilités, indépendamment de savoir quels changements se produiront à l'avenir. Bob Martin va généralement plus loin, voir le lien que j'ai donné dans mes commentaires ci-dessous.

EDIT: à votre commentaire: La principale responsabilité de la fonction externe dans l'exemple ci-dessus n'est pas d'imprimer sur un périphérique spécifique, ou de formater le document - c'est d' intégrer le flux de travail d'impression . Ainsi, au niveau abstraction de la fonction externe, une nouvelle exigence comme "les documents ne doivent plus être formatés" ou "les documents doivent être envoyés au lieu d'être imprimés" n'est que "la même raison" - à savoir "le flux de travail d'impression a changé". Si nous parlons de choses comme ça, il est important de s'en tenir au bon niveau d'abstraction .

Doc Brown
la source
Je développe généralement toujours avec TDD, donc dans mon exemple, je n'aurais pas pu physiquement conserver toute cette logique dans un seul module car il serait impossible de le tester. Ceci est juste un sous-produit de TDD et non pas parce que j'applique délibérément SRP. Mon exemple avait des responsabilités distinctes assez claires, donc ce n'est peut-être pas un bon exemple. Je pense que ce que je demande est, pouvez-vous écrire un nouveau morceau de code et dire sans équivoque, oui, cela ne viole pas SRP? Les «raisons de changer» ne sont-elles pas essentiellement définies par l'entreprise?
SeeNoWeevil
3
@thecapsaicinkid: oui, vous pouvez (au moins par refactoring immédiat). Mais vous obtiendrez des fonctions très, très petites - et tous les programmeurs n'aiment pas cela. Voir cet exemple: sites.google.com/site/unclebobconsultingllc/…
Doc Brown
Si vous appliquiez le PÉR en anticipant les raisons du changement, dans votre exemple, je pourrais toujours soutenir qu'il y a plus d'un seul changement de raison. L'entreprise peut décider qu'elle ne souhaite plus formater un document, puis décider ultérieurement qu'elle souhaite qu'il soit envoyé par e-mail au lieu d'être imprimé. EDIT: lisez simplement le lien, et bien que je n'aime pas particulièrement le résultat final, «Extraire jusqu'à ce que vous ne puissiez plus extraire» est beaucoup plus logique et moins ambigu que «une seule raison de changer». Pas très pragmatique cependant.
SeeNoWeevil
1
@thecapsaicinkid: voir ma modification. La fonction principale de la fonction externe n'est pas d'imprimer sur un périphérique spécifique, ni de formater le document - c'est d'intégrer le flux d'impression. Et lorsque ce flux de travail change, c'est la seule et unique raison pour laquelle la fonction va changer
Doc Brown
Votre commentaire sur le maintien du bon niveau d'abstraction semble être ce qui me manquait. Par exemple, j'ai une classe que je décrirais comme «Crée des structures de données à partir d'un tableau JSON». Cela ressemble à une seule responsabilité pour moi. Boucle à travers les objets dans un tableau JSON et les mappe en POJO. Si je m'en tiens au même niveau d'abstraction que ma description, il est difficile de prétendre qu'il a plus d'une raison de changer, à savoir «Comment JSON est mappé à l'objet». Étant moins abstrait, je pourrais dire que cela a plus d'une raison, par exemple, comment je mappe les changements de champs de date, comment les valeurs numériques sont mappées en jours, etc.
SeeNoWeevil
7

Je pense que vous avez mal compris SRP.

La seule raison du changement n'est PAS de changer le code mais de faire ce que fait votre code.

Pieter B
la source
3

Je pense que la définition de SRP comme "ayant une raison de changer" est trompeuse pour exactement cette raison. Prenez-le exactement à sa valeur nominale: le principe de responsabilité unique dit qu'une classe ou une fonction devrait avoir exactement une responsabilité. N'avoir qu'une seule raison de changer est un effet secondaire de ne faire qu'une chose pour commencer. Il n'y a aucune raison pour laquelle vous ne pouvez pas au moins faire un effort vers une responsabilité unique dans votre code sans savoir quoi que ce soit pourrait changer à l'avenir.

L'un des meilleurs indices pour ce genre de chose est lorsque vous choisissez des noms de classe ou de fonction. S'il n'est pas immédiatement évident que la classe doit être nommée, ou si le nom est particulièrement long / complexe, ou si le nom utilise des termes génériques comme "gestionnaire" ou "utilitaire", alors il s'agit probablement d'une violation de SRP. De même, lors de la documentation de l'API, il devrait devenir rapidement évident si vous violez SRP en fonction de la fonctionnalité que vous décrivez.

Il y a, bien sûr, des nuances à SRP que vous ne pouvez pas connaître plus tard dans le projet - ce qui semblait être une seule responsabilité s'est avéré être deux ou trois. Ce sont des cas où vous devrez refactoriser pour implémenter SRP. Mais cela ne signifie pas que SRP doit être ignoré jusqu'à ce que vous obteniez une demande de modification; qui va à l'encontre du but de SRP!

Pour parler directement de votre exemple, pensez à documenter votre méthode d'impression. Si vous souhaitez dire « cette méthode formate les données pour l' impression et l'envoie à l'imprimante, » qui et est ce que vous obtient: ce n'est pas une responsabilité unique, qui est deux responsabilités: la mise en forme et l' envoi à l'imprimante. Si vous reconnaissez cela et les divisez en deux fonctions / classes, alors lorsque vos demandes de changement sont arrivées, vous n'aviez déjà qu'une seule raison pour chaque section de changer.

Adrian
la source
3

Un exemple serait un morceau de code qui imprime un document. Une demande vient le modifier pour imprimer au format PDF, puis une deuxième demande est faite pour le modifier afin d'appliquer une mise en forme différente au document. À ce stade, vous avez la preuve de plus d'une seule raison de changer (et d'une violation de SRP) et devez effectuer le refactoring approprié.

Je me suis tant de fois tiré une balle dans le pied en passant trop de temps à adapter le code à ces changements. Au lieu d'imprimer simplement le fichu stupide PDF.

Refactor pour réduire le code

Le modèle à usage unique peut créer un ballonnement de code. Où les packages sont pollués par de petites classes spécifiques qui créent une pile de code poubelle qui n'a pas de sens individuellement. Vous devez ouvrir des dizaines de fichiers source juste pour comprendre comment cela arrive à la partie d'impression. En plus de cela, il peut y avoir des centaines sinon des milliers de lignes de code qui sont en place juste pour exécuter 10 lignes de code qui font l'impression proprement dite.

Créer un oeil de boeuf

Le modèle à usage unique était destiné à réduire le code source et à améliorer la réutilisation du code. Il était destiné à créer une spécialisation et des implémentations spécifiques. Une sorte de bullseyecode source pour vous go to specific tasks. Lorsqu'il y avait un problème d'impression, vous saviez exactement où aller pour le corriger.

L'utilisation unique ne signifie pas une fracturation ambiguë

Oui, vous disposez d'un code qui imprime déjà un document. Oui, vous devez maintenant modifier le code pour imprimer également des PDF. Oui, vous devez maintenant modifier la mise en forme du document.

Êtes-vous sûr que le usagechangement a considérablement changé?

Si la refactorisation entraîne une généralisation excessive des sections du code source. Au point que l'intention d'origine de printing stuffn'est plus explicite, vous avez créé une fracture ambiguë dans le code source.

Le nouveau gars pourra-t-il comprendre cela rapidement?

Conservez toujours votre code source dans l'organisation la plus simple à comprendre.

Ne soyez pas horloger

Beaucoup trop de fois, j'ai vu des développeurs mettre un oculaire, et se concentrer sur les petits détails au point que personne d'autre ne pourrait remettre les morceaux en place en cas de désagrégation.

entrez la description de l'image ici

Reactgular
la source
2

Une raison du changement est, en fin de compte, un changement de spécification ou d'informations sur l'environnement dans lequel l'application s'exécute. Ainsi, un principe de responsabilité unique vous dit d'écrire chaque composant (classe, fonction, module, service ...) pour qu'il prenne en compte le moins possible la spécification et l'environnement d'exécution.

Puisque vous connaissez les spécifications et l'environnement lorsque vous écrivez le composant, vous pouvez appliquer le principe.

Si vous considérez l'exemple de code qui imprime un document. Vous devez déterminer si vous pouvez définir le modèle de mise en page sans considérer que le document se retrouvera au format PDF. Vous pouvez, donc SRP vous dit que vous devriez.

Bien sûr, YAGNI vous dit que vous ne devriez pas. Vous devez trouver un équilibre entre les principes de conception.

Jan Hudec
la source
2

Flup se dirige dans la bonne direction. Le "principe de responsabilité unique" s'appliquait à l'origine aux procédures. Par exemple, Dennis Ritchie dirait qu'une fonction devrait faire une chose et bien le faire. Ensuite, en C ++, Bjarne Stroustrup dirait qu'une classe devrait faire une chose et bien le faire.

Notez que, sauf comme règles de base, ces deux-là n'ont formellement rien ou presque rien à voir l'un avec l'autre. Ils ne répondent qu'à ce qui est pratique à exprimer dans le langage de programmation. Eh bien, c'est quelque chose. Mais c'est une histoire très différente de celle vers laquelle se dirige le flup.

Les implémentations modernes (c'est-à-dire agiles et DDD) se concentrent davantage sur ce qui est important pour l'entreprise que sur ce que le langage de programmation peut exprimer. La partie surprenante est que les langages de programmation n'ont pas encore rattrapé leur retard. Les anciens langages de type FORTRAN capturent des responsabilités qui correspondent aux principaux modèles conceptuels de l'époque: les processus appliqués à chaque carte lors de son passage dans le lecteur de carte, ou (comme en C) le traitement qui accompagnait chaque interruption. Viennent ensuite les langages ADT, qui ont mûri au point de capturer ce que les DDD réinventeront plus tard comme étant important (bien que Jim Neighbours ait compris la majeure partie de cela, publié et utilisé en 1968): ce que nous appelons aujourd'hui des classes . (Ce ne sont PAS des modules.)

Cette étape était moins une évolution qu'une balançoire pendulaire. Alors que le pendule basculait vers les données, nous avons perdu la modélisation de cas d'utilisation inhérente à FORTRAN. C'est très bien lorsque votre objectif principal concerne les données ou les formes sur un écran. C'est un excellent modèle pour des programmes comme PowerPoint, ou du moins pour ses opérations simples.

Ce qui s'est perdu, ce sont les responsabilités du système . Nous ne vendons pas les éléments de DDD. Et nous ne comprenons pas bien les méthodes. Nous vendons des responsabilités système. À un certain niveau, vous devez concevoir votre système autour du principe de responsabilité unique.

Donc, si vous regardez des gens comme Rebecca Wirfs-Brock, ou moi, qui parlions des méthodes de classe, nous parlons maintenant en termes de cas d'utilisation. C'est ce que nous vendons. Ce sont les opérations du système. Un cas d'utilisation doit avoir une seule responsabilité. Un cas d'utilisation est rarement une unité architecturale. Mais tout le monde essayait de faire comme si. Témoin le peuple SOA, par exemple.

C'est pourquoi je suis enthousiasmé par l'architecture DCI de Trygve Reenskaug - qui est décrite dans le livre sur l'architecture Lean ci-dessus. Il donne enfin une certaine stature à ce qui était auparavant une obéissance arbitraire et mystique à la «responsabilité unique» - comme on le trouve dans la plupart des arguments ci-dessus. Cette stature est liée aux modèles mentaux humains: les utilisateurs finaux en premier ET les programmeurs en second. Cela concerne des préoccupations commerciales. Et, presque par hasard, il résume le changement alors que le flup nous interpelle.

Le principe de responsabilité unique tel que nous le connaissons est soit un dinosaure resté de ses jours d'origine, soit un cheval de bataille que nous utilisons comme substitut à la compréhension. Vous devez laisser quelques-uns de ces chevaux de passe-temps derrière pour faire un excellent logiciel. Et cela nécessite de sortir des sentiers battus. Garder les choses simples et faciles à comprendre ne fonctionne que lorsque le problème est simple et facile à comprendre. Je ne suis pas très intéressé par ces solutions: elles ne sont pas typiques et ce n'est pas là que réside le défi.

Chape
la source
2
En lisant ce que vous avez écrit, quelque part en cours de route, j'ai tout simplement perdu de vue ce dont vous parlez. Les bonnes réponses ne traitent pas la question comme le point de départ d'une randonnée dans les bois, mais plutôt comme un thème précis auquel lier toute l'écriture.
Donal Fellows
1
Ah, vous en faites partie, comme l'un de mes anciens managers. "Nous ne voulons pas le comprendre: nous voulons l'améliorer!" La question thématique clé ici est une question de principe: c'est le "P" dans "SRP". J'aurais peut-être répondu directement à la question si c'était la bonne question: ce n'était pas le cas. Vous pouvez en discuter avec qui a déjà posé la question.
Faites face
Il y a une bonne réponse enterrée ici quelque part. Je pense ...
RubberDuck
0

Oui, le principe de responsabilité unique devrait être appliqué au nouveau code.

Mais! Qu'est-ce qu'une responsabilité?

Est-ce que "imprimer un rapport est une responsabilité"? Je crois que la réponse est "Peut-être".

Essayons d'utiliser la définition de SRP comme "n'ayant qu'une seule raison de changer".

Supposons que vous ayez une fonction qui imprime des rapports. Si vous avez deux modifications:

  1. changer cette fonction car votre rapport doit avoir un fond noir
  2. changer cette fonction parce que vous devez imprimer en pdf

Ensuite, le premier changement est "changer le style de rapport", l'autre est "changer le format de sortie du rapport" et maintenant vous devez les mettre dans deux fonctions différentes car ce sont des choses différentes.

Mais si votre deuxième changement aurait été:

2b. changer cette fonction parce que votre rapport a besoin d'une police différente

Je dirais que les deux changements consistent à "changer le style de rapport" et qu'ils peuvent rester dans une seule fonction.

Alors, où en sommes-nous? Comme d'habitude, vous devriez essayer de garder les choses simples et faciles à comprendre. Si changer la couleur d'arrière-plan signifie 20 lignes de code et changer la police signifie 20 lignes de code, faites-en de nouveau deux fonctions. S'il s'agit d'une ligne chacun, conservez-la en une.

Sarien
la source
0

Lorsque vous concevez un nouveau système, il est sage de considérer le type de changements que vous devrez peut-être apporter au cours de sa durée de vie et le coût de ceux-ci étant donné l'architecture que vous mettez en place. La division de votre système en modules est une décision coûteuse de se tromper.

Une bonne source d'information est le modèle mental du chef des experts du domaine de l'entreprise. Prenons l'exemple du document, de la mise en forme et du pdf. Les experts du domaine vous diront probablement qu'ils mettent en forme leurs lettres à l'aide de modèles de documents. Soit à l'arrêt, soit en Word ou autre. Vous pouvez récupérer ces informations avant de commencer à coder et à les utiliser dans votre conception.

Une bonne lecture de ces choses: Lean Architecture par Coplien

flup
la source
0

"Imprimer" ressemble beaucoup à "voir" dans MVC. Quiconque comprend les bases des objets comprendrait cela.

C'est une responsabilité du système . Il est implémenté comme un mécanisme - MVC - qui implique une imprimante (la vue), la chose imprimée (le module) et la demande et les options de l'imprimante (du contrôleur).

Essayer de localiser cela comme une responsabilité de classe ou de module est insensé et reflète une pensée vieille de 30 ans. Nous avons beaucoup appris depuis lors, et cela est amplement démontré dans la littérature et dans le code des programmeurs matures.

Chape
la source
0

N'est-ce pas une meilleure idée de ne vraiment commencer à appliquer SRP que lorsque des demandes de changement de code commencent à arriver?

Idéalement, vous aurez déjà une bonne idée des responsabilités des différentes parties du code. Répartissez-vous en responsabilités en fonction de vos premiers instincts, en tenant éventuellement compte de ce que les bibliothèques que vous utilisez veulent faire (déléguer une tâche, une responsabilité à une bibliothèque est généralement une bonne chose à faire, à condition que la bibliothèque puisse réellement faire la tâche ). Ensuite, affinez votre compréhension des responsabilités en fonction de l'évolution des exigences. Mieux vous comprenez le système au départ, moins vous avez besoin de changer fondamentalement les affectations de responsabilité (même si parfois vous découvrez qu'une responsabilité est mieux divisée en sous-responsabilités).

Non pas que vous deviez vous en soucier longtemps. Une caractéristique clé du code est qu'il peut être modifié plus tard, vous n'avez pas besoin de le faire correctement la première fois. Essayez simplement de vous améliorer au fil du temps en apprenant quel type de responsabilité les formes ont afin que vous puissiez faire moins d'erreurs à l'avenir.

Un exemple serait un morceau de code qui imprime un document. Une demande vient le modifier pour imprimer au format PDF, puis une deuxième demande est faite pour le modifier afin d'appliquer une mise en forme différente au document. À ce stade, vous avez la preuve de plus d'une seule raison de changer (et d'une violation de SRP) et devez effectuer le refactoring approprié.

Ceci est strictement une indication que la responsabilité globale - «imprimer» le code - a des sous-responsabilités et devrait être divisée en morceaux. Ce n'est pas une violation de SRP en soi, mais plutôt une indication que le partitionnement (peut-être en sous-tâches de «formatage» et de «rendu») est probablement nécessaire. Pouvez-vous décrire clairement ces responsabilités afin de comprendre ce qui se passe au sein des sous-tâches sans regarder leur mise en œuvre? Si vous le pouvez, ce sont probablement des divisions raisonnables.

Cela pourrait aussi être plus clair si nous regardons un exemple réel simple. Considérons la sort()méthode d'utilité dans java.util.Arrays. Qu'est ce que ça fait? Il trie un tableau, et c'est tout ce qu'il fait. Il n'imprime pas les éléments, il ne trouve pas le membre le plus apte moralement, il ne siffle pas Dixie . Il trie simplement un tableau. Vous n'avez pas non plus besoin de savoir comment. Le tri est la seule responsabilité de cette méthode. (En fait, il existe de nombreuses méthodes de tri en Java pour des raisons techniques quelque peu laides liées aux types primitifs; vous n'avez cependant pas à y prêter attention, car elles ont toutes des responsabilités équivalentes.)

Faites vos méthodes, vos classes, vos modules, faites-leur avoir un rôle si clairement désigné dans la vie. Il réduit le montant que vous devez comprendre à la fois, et c'est ce qui vous permet de gérer la conception et la maintenance d'un grand système.

Associés Donal
la source