En utilisant le principe de responsabilité unique, qu'est-ce qui constitue une «responsabilité»?


198

Il semble assez clair que "Principe de responsabilité unique" ne signifie pas "une seule chose". Voilà à quoi servent les méthodes.

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

Bob Martin dit que "les classes ne devraient avoir qu'une seule raison de changer". Mais c’est difficile de garder l’esprit au clair si vous êtes un nouveau programmeur chez SOLID.

J'ai écrit une réponse à une autre question , dans laquelle je suggérais que les responsabilités ressemblaient à des titres d'emploi, et j'ai dansé autour du sujet en utilisant une métaphore de restaurant pour illustrer mon propos. Mais cela n'énonce toujours pas un ensemble de principes que quelqu'un pourrait utiliser pour définir les responsabilités de leurs classes.

Alors, comment fais-tu? Comment déterminez-vous les responsabilités que chaque classe devrait avoir et comment définissez-vous une responsabilité dans le contexte du PÉR?


28
Poster pour Code Review et être déchiré :-D
Jörg W Mittag Le

8
@ JörgWMittag Hé maintenant, ne faites pas peur aux gens :)
Flambino

118
Des questions comme celle-ci de membres vétérans démontrent que les règles et les principes auxquels nous essayons de nous conformer ne sont en aucun cas simples ni simples . Ils sont [en quelque sorte] contradictoires et mystiques ... comme devrait l' être tout bon ensemble de règles . Et, j'aimerais croire que des questions comme celle-ci humilient les sages, et donner de l'espoir à ceux qui se sentent désespérément stupides. Merci Robert!
svidgen

41
Je me demande si cette question aurait été rejetée immédiatement + duplicata marquée si elle avait été postée par un noob :)
Andrejs

9
@ Munn: ou en d'autres termes - un grand représentant attire encore plus de représentants, car personne n'a annulé les préjugés humains fondamentaux sur stackexchange
Andrejs

Réponses:


117

Une façon de comprendre cela consiste à imaginer des modifications potentielles des exigences dans les projets futurs et à vous demander ce que vous devrez faire pour les réaliser.

Par exemple:

Nouvelle exigence commerciale: les utilisateurs situés en Californie bénéficient d'un rabais spécial.

Exemple de "bon" changement: Je dois modifier le code dans une classe qui calcule les remises.

Exemple de changements incorrects: je dois modifier le code de la classe User et ce changement aura un effet en cascade sur les autres classes qui utilisent la classe User, y compris les classes n’ayant rien à voir avec des remises, telles que l’inscription, l’énumération et la gestion.

Ou:

Nouvelle exigence non fonctionnelle: nous allons commencer à utiliser Oracle au lieu de SQL Server

Exemple de bon changement: Il suffit de modifier une seule classe dans la couche d'accès aux données, qui détermine comment conserver les données dans les DTO.

Mauvais changement: je dois modifier toutes les classes de ma couche de gestion, car elles contiennent une logique spécifique à SQL Server.

L'idée est de minimiser l'empreinte des modifications potentielles futures, en limitant les modifications de code à une zone de code par zone de modification.

Au minimum, vos cours doivent séparer les préoccupations logiques des préoccupations physiques. Un grand ensemble d'exemples se trouvent dans l' System.IOespace de noms: là , nous pouvons trouver un divers types de flux physiques (par exemple FileStream, MemoryStreamou NetworkStream) et divers lecteurs et écrivains ( BinaryWriter, TextWriter) qui travaillent à un niveau logique. En les séparant de cette façon, nous évitons l' explosion combinatoires: au lieu d' avoir besoin FileStreamTextWriter, FileStreamBinaryWriter, NetworkStreamTextWriter, NetworkStreamBinaryWriter, MemoryStreamTextWriteret MemoryStreamBinaryWriter, vous venez de brancher l'écrivain et le flux et vous pouvez avoir ce que vous voulez. Ensuite, nous pouvons ajouter par exemple un XmlWriter, sans avoir à le réimplémenter séparément pour la mémoire, les fichiers et le réseau.


34
Bien que je sois d’accord pour penser à l’avenir, il existe des principes tels que YAGNI et des méthodologies telles que TDD qui suggèrent le contraire.
Robert Harvey

87
YAGNI nous dit de ne pas construire des choses dont nous n'avons pas besoin aujourd'hui. Cela ne dit pas de ne pas construire des choses de manière extensible. Voir aussi principe ouvert / fermé , qui stipule que "les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification."
John Wu

18
@JohnW: +1 pour votre commentaire YAGNI seul. Je n'arrive pas à croire à quel point je dois expliquer aux gens que YAGNI n'est pas une excuse pour construire un système rigide et inflexible qui ne peut pas réagir au changement - paradoxalement, le contraire de l'objectif recherché par SRP et les principes Open / Closed.
Greg Burghardt

36
@JohnWu: Je ne suis pas d'accord, YAGNI nous dit exactement de ne pas construire des choses dont nous n'avons pas besoin aujourd'hui. La lisibilité et les tests, par exemple, font toujours partie du programme "aujourd'hui", et YAGNI n'est jamais une excuse pour ne pas ajouter de structure ni de points d'injection. Cependant, dès que "l'extensibilité" ajoute un coût important dont les avantages ne sont pas évidents "aujourd'hui", YAGNI entend éviter ce type d'extensibilité, ce dernier conduisant à une ingénierie excessive.
Doc Brown

9
@JohnWu Nous sommes passés de SQL 2008 à 2012. Deux requêtes au total devaient être modifiées. Et de SQL Auth à Trusted? Pourquoi serait-ce même un changement de code? changer la connectionString dans le fichier de configuration suffit. Encore une fois, YAGNI. YAGNI et SRP sont parfois des problèmes concurrents et vous devez déterminer lequel présente le meilleur rapport coût / avantages.
Andy

76

Sur le plan pratique, les responsabilités sont limitées par les éléments susceptibles de changer. Ainsi, il n’existe malheureusement pas de méthode scientifique ou théorique pour arriver à ce qui constitue une responsabilité. C'est un jugement.

Il est sur ce qui, dans votre expérience , est susceptible de changer.

Nous avons tendance à appliquer le langage du principe dans une rage hyperbolique, littérale et zélée. Nous avons tendance à diviser les classes parce qu'elles peuvent changer ou selon des lignes qui nous aident simplement à résoudre les problèmes. (Cette dernière raison n'est pas intrinsèquement mauvaise.) Mais, le SRP n'existe pas pour lui-même; il est au service de la création de logiciels maintenables.

Encore une fois, si les divisions ne sont pas motivées par des changements probables , elles ne sont pas vraiment au service du PÉR 1, si YAGNI est plus applicable. Les deux servent le même objectif ultime. Et les deux sont des questions de jugement - espérons un jugement expérimenté .

Lorsque Oncle Bob écrit à ce sujet, il suggère de penser à la "responsabilité" en termes de "qui demande le changement". En d'autres termes, nous ne voulons pas que le parti A perde son emploi, car le parti B a demandé un changement.

Lorsque vous écrivez un module logiciel, vous voulez vous assurer que, lorsque des modifications sont demandées, ces modifications ne peuvent émaner que d'une seule personne, ou plutôt d'un seul groupe de personnes étroitement liées représentant une seule fonction métier étroitement définie. Vous souhaitez isoler vos modules des complexités de l'organisation dans son ensemble et concevoir vos systèmes de manière à ce que chaque module soit responsable (réponde) aux besoins de cette seule fonction métier. ( Oncle Bob - Le principe de responsabilité unique )

Les développeurs bons et expérimentés auront une idée des changements probables. Et cette liste mentale variera quelque peu selon l'industrie et l'organisation.

Ce qui constitue une responsabilité dans votre application particulière, au sein de votre organisation particulière, est finalement une question de jugement expérimenté . Il s'agit de ce qui est susceptible de changer. Et, dans un sens, il s'agit de savoir à qui appartient la logique interne du module.


1. Pour être clair, cela ne signifie pas que ce sont de mauvaises divisions. Ce pourraient être de grandes divisions qui améliorent considérablement la lisibilité du code. Cela signifie simplement qu'ils ne sont pas motivés par le PÉR.


11
Meilleure réponse, et cite en fait les pensées de l'oncle Bob. En ce qui concerne ce qui est susceptible de changer, tout le monde accorde beaucoup d'importance aux E / S: "Et si on change de base de données?" ou "si nous passons de XML à JSON?" Je pense que c'est généralement mal avisé. La vraie question devrait être "que se passe-t-il si nous devons changer cet int en un float, ajouter un champ et changer cette chaîne en une liste de chaînes?"
user949300

2
C'est de la triche. La responsabilité unique en soi n'est qu'un moyen proposé "d'isoler le changement". Expliquer que vous devez isoler les modifications pour conserver la responsabilité "unique" ne vous indique pas comment procéder, mais explique simplement l'origine de l'exigence.
Basilevs

6
@Basilevs J'essaie de combler le manque que vous constatez dans cette réponse - sans parler de la réponse de mon oncle Bob! Mais peut-être dois-je clarifier que le PRS ne vise pas à garantir qu'un "changement" n'aura un impact que sur une classe. Il s’agit de veiller à ce que chaque classe réponde à "un seul changement". ... Il s'agit d'essayer de tirer vos flèches de chaque classe à un seul propriétaire. Pas de chaque propriétaire à une seule classe.
svidgen

2
Merci d'avoir fourni une réponse pragmatique! Même Oncle Bob met en garde contre le respect zélé des principes SOLID dans l' architecture agile . Je n'ai pas la citation à portée de main, mais il dit essentiellement que le partage des responsabilités augmente de manière inhérente le niveau d'abstraction dans votre code et que toute abstraction a un coût. Veillez donc à ce que les avantages de suivre SRP (ou d'autres principes) l'emportent sur les coûts. d'ajouter plus d'abstraction. (suite commentaire suivant)
Michael L.

4
C'est pourquoi nous devrions mettre le produit devant le client aussi tôt et aussi souvent qu'il est raisonnable pour qu'il force la modification de notre conception et nous permet de voir les zones susceptibles de changer dans ce produit. De plus, il prévient que nous ne pouvons pas nous protéger de tout type de changement. Pour toute application, certains types de modifications seront difficiles à effectuer. Nous devons nous assurer que ce sont les changements les moins susceptibles de se produire.
Michael L.

29

Je suis "les cours ne devraient avoir qu'une seule raison de changer".

Pour moi, cela signifie de penser à des projets compliqués que mon propriétaire de produit pourrait proposer ("Nous devons prendre en charge le mobile!", "Nous devons accéder au cloud!", "Nous devons prendre en charge le chinois!"). De bonnes conceptions limiteront l’impact de ces projets sur de plus petites zones et les rendront relativement faciles à réaliser. De mauvaises conceptions impliquent d'utiliser beaucoup de code et de procéder à de nombreuses modifications risquées.

L’expérience est la seule chose que j’ai trouvée pour bien évaluer la probabilité de ces stratagèmes délirants - car faciliter l’un des deux, en rendre deux autres plus difficiles - et évaluer la qualité d’une conception. Les programmeurs expérimentés peuvent imaginer ce qu'ils devraient faire pour changer le code, ce qui traîne pour les mordre dans le cul, et quelles astuces facilitent les choses. Les programmeurs expérimentés ont un bon pressentiment à quel point ils sont foutus lorsque le responsable du produit leur demande des trucs fous.

Pratiquement, je trouve que les tests unitaires aident ici. Si votre code est inflexible, il sera difficile à tester. Si vous ne pouvez pas injecter de simulacres ou d'autres données de test, vous ne pourrez probablement pas injecter ce SupportChinesecode.

Une autre mesure approximative est le pas d'ascenseur. Les ascenseurs traditionnels sont "si vous étiez dans un ascenseur avec un investisseur, pouvez-vous lui vendre une idée?". Les entreprises en démarrage doivent avoir une description simple et succincte de leurs activités - de leurs priorités. De même, les classes (et les fonctions) doivent avoir une description simple de ce qu’elles font . Non "cette classe implémente certains fubar tels que vous pouvez les utiliser dans ces scénarios spécifiques". Vous pouvez dire quelque chose à un autre développeur: "Cette classe crée des utilisateurs". Si vous ne pouvez pas communiquer cela aux autres développeurs, vous allez avoir des bogues.


Parfois, vous allez implémenter ce que vous pensiez être un changement désordonné, et il s'avère simple, ou un petit refactor le simplifie et ajoute des fonctionnalités utiles en même temps. Mais oui, généralement, vous pouvez voir des problèmes à venir.

16
Je suis un fervent défenseur de l'idée du "pitch pitch". S'il est difficile d'expliquer ce qu'une classe fait en une phrase ou deux, vous êtes en territoire à risque.
Ivan

1
Vous touchez un point important: la probabilité de ces projets fous varie énormément d’un propriétaire de projet à l’autre. Vous devez compter non seulement sur votre expérience générale, mais également sur votre connaissance du porteur de projet. J'ai travaillé pour des gens qui voulaient réduire notre sprint à une semaine et qui ne pouvaient toujours pas éviter de changer de direction à mi-sprint.
Kevin Krumwiede

1
En plus des avantages évidents, la documentation de votre code à l'aide d '"ascenseurs" sert également à vous aider à réfléchir à ce que votre code fait à l'aide d'un langage naturel, ce qui me semble utile pour découvrir de multiples responsabilités.
Alexander

1
@KevinKrumwiede C'est à quoi servent les méthodologies "Poulet qui tourne la tête coupée" et "Chasse à l'oie sauvage"!

26

Personne ne sait. Ou du moins, nous ne pouvons pas nous mettre d’accord sur une définition. C’est ce qui rend SPR (et d’autres principes SOLID) assez controversé.

Je dirais que pouvoir comprendre ce qui est ou non une responsabilité est l’une des compétences que le développeur de logiciels doit acquérir au cours de sa carrière. Plus vous écrivez et révisez de code, plus vous aurez d’expérience pour déterminer s’il s’agit d’une responsabilité unique ou à responsabilités multiples. Ou si la responsabilité unique est fractionnée entre différentes parties du code.

Je dirais que l’objectif principal du SRP n’est pas d’être une règle stricte. C'est pour nous rappeler de veiller à la cohésion dans le code et de toujours faire un effort conscient pour déterminer quel code est cohérent et ce qui ne l'est pas.


20
Les nouveaux programmeurs ont tendance à traiter SOLID comme s'il s'agissait d'un ensemble de lois, ce qui n'est pas le cas. Il s’agit simplement d’un regroupement de bonnes idées destinées à aider les personnes à améliorer la conception de leurs classes. Hélas, les gens ont tendance à prendre ces principes trop au sérieux. J'ai récemment vu une offre d'emploi citant SOLID comme l'une des exigences du poste.
Robert Harvey

9
+42 pour le dernier paragraphe. Comme @RobertHarvey le dit, des choses comme SPR, SOLID et YAGNI ne doivent pas être considérées comme des " règles absolues ", mais comme des principes généraux de "bons conseils". Entre eux (et les autres), les conseils seront parfois contradictoires, mais équilibrer ces conseils (au lieu de suivre un ensemble de règles strictes) vous guidera (avec le temps, à mesure que votre expérience grandit) pour produire de meilleurs logiciels. Il ne devrait y avoir qu'une seule "règle absolue" dans le développement logiciel: " Il n'y a pas de règles absolues ".
TripeHound

C'est une très bonne clarification sur un aspect du SRP. Cependant, même si les principes SOLID ne sont pas des règles strictes, ils ne sont pas très utiles si personne ne comprend ce qu’ils signifient - et encore moins si votre affirmation selon laquelle "personne ne le sait" est réellement vraie! ... il est logique qu'ils soient difficiles à comprendre. Comme pour toute compétence, il y a quelque chose qui distingue le bon du moins bon! Mais ... "personne ne sait" en fait un rituel de bizutage. (Et je ne crois pas que ce soit l'intention de SOLID!)
svidgen Le

3
Par "Personne ne sait", j'espère que @Euphoric signifie simplement qu’il n’existe pas de définition précise qui conviendra à tous les cas d’utilisation. C'est quelque chose qui nécessite un certain jugement. Je pense que l’un des meilleurs moyens de déterminer vos responsabilités consiste à itérer rapidement et à laisser votre code vous le dire . Recherchez des "odeurs" indiquant que votre code n'est pas facilement maintenable. Par exemple, lorsqu'une modification d'une seule règle métier commence à avoir des effets en cascade via des classes apparemment non liées, vous avez probablement une violation de SRP.
Michael L.

1
@TripeHound et tous les autres qui ont souligné que toutes ces "règles" n'existent pas pour définir la véritable religion du développement, mais pour augmenter les chances de développement de logiciels maintenables. Méfiez-vous des «meilleures pratiques» si vous ne pouvez pas expliquer en quoi cela favorise les logiciels maintenables, améliore la qualité ou augmente l'efficacité du développement.
Michael L.

5

Je pense que le terme "responsabilité" est utile en tant que métaphore, car il nous permet d'utiliser le logiciel pour déterminer dans quelle mesure le logiciel est organisé. En particulier, je me concentrerais sur deux principes:

  • La responsabilité est à la mesure de l'autorité.
  • Aucune entité ne devrait être responsable de la même chose.

Ces deux principes nous ont permis de nous décharger de nos responsabilités, car ils se jouent mutuellement. Si vous autorisez un élément de code à faire quelque chose pour vous, il doit être responsable de ce qu'il fait. Cela entraîne la responsabilité d'une classe qui pourrait devoir grandir, élargissant c'est "une raison de changer" à des domaines de plus en plus larges. Cependant, à mesure que vous élargissez les choses, vous commencez naturellement à vous retrouver dans des situations où plusieurs entités sont responsables de la même chose. Cela soulève de nombreux problèmes de responsabilité dans la vie réelle, donc c’est sûrement un problème de codage. En conséquence, ce principe entraîne la réduction des portées lorsque vous divisez la responsabilité en parcelles non dupliquées.

Outre ces deux principes, un troisième principe semble raisonnable:

  • La responsabilité peut être déléguée

Considérez un programme fraichement mis à jour… une ardoise vierge. Au début, vous n'avez qu'une seule entité, le programme dans son ensemble. Il est responsable de ... tout. Naturellement, à un moment donné, vous commencerez à déléguer des responsabilités à des fonctions ou à des classes. À ce stade, les deux premières règles entrent en jeu, vous obligeant à équilibrer cette responsabilité. Le programme de niveau supérieur est toujours responsable de la production globale, tout comme un responsable est responsable de la productivité de son équipe, mais chaque sous-entité se voit déléguer la responsabilité, ce qui lui donne le pouvoir de s’acquitter de cette responsabilité.

En prime, cela rend SOLID particulièrement compatible avec tous les développements de logiciels d'entreprise nécessaires. Toutes les entreprises de la planète ont une idée de la manière de déléguer des responsabilités, et elles ne sont pas toutes d'accord. Si vous déléguez des responsabilités dans votre logiciel d'une manière qui rappelle la délégation de votre entreprise, il sera beaucoup plus facile pour les futurs développeurs de se familiariser avec la façon dont vous faites les choses dans cette entreprise.


Je ne suis pas sûr à 100% que cela l'explique pleinement. Mais, je pense qu'expliquer la "responsabilité" en ce qui concerne "l'autorité" est un moyen perspicace de la formuler! (+1)
svidgen

Pirsig a déclaré: "Vous avez tendance à intégrer vos problèmes à la machine", ce qui me donne une pause.

@nocomprende Vous avez également tendance à intégrer vos atouts à la machine. Je dirais que quand vos forces et vos faiblesses sont les mêmes choses, c'est là que ça devient intéressant.
Cort Ammon

5

Oncle Bob donne cet exemple amusant dans cette conférence à Yale :

Entrez la description de l'image ici

Il dit que cela Employeea trois raisons de changer, trois sources d'exigences de changement, et donne cette explication amusante et ironique , mais illustrative néanmoins:

  • Si la CalcPay()méthode comporte une erreur et coûte des millions de dollars à l'entreprise, le CFO vous licenciera .

  • Si la ReportHours()méthode comporte une erreur et coûte des millions de dollars à l'entreprise, le COO vous licenciera .

  • Si la WriteEmmployee(méthode a une erreur qui entraîne l’effacement de nombreuses données et coûte des millions de dollars à la société, le CTO vous renverra .

Par conséquent , si vous avez trois administrateurs différents de niveau C susceptibles de vous renvoyer pour des erreurs coûteuses dans la même classe, cela signifie que la classe a trop de responsabilités.

Il donne cette solution qui résout la violation de SRP, mais doit encore résoudre la violation de DIP qui n'est pas montrée dans la vidéo.

Entrez la description de l'image ici


Cet exemple ressemble plus à une classe qui a de mauvaises responsabilités.
Robert Harvey

4
@RobertHarvey Quand une classe a trop de responsabilités, cela signifie que les responsabilités supplémentaires sont des responsabilités incorrectes .
Tulains Córdova

5
J'entends ce que vous dites, mais je ne trouve pas cela convaincant. Il y a une différence entre une classe ayant trop de responsabilités et une classe faisant quelque chose qu'elle n'a pas du tout affaire. Cela peut sembler pareil, mais ça ne l’est pas; compter les cacahuètes n'est pas la même chose que les appeler des noix. C'est le principe d'Oncle Bob et l'exemple d'Oncle Bob, mais si c'était suffisamment descriptif, nous n'aurions pas besoin de cette question du tout.
Robert Harvey

@ RobertHarvey, quelle est la différence? Ces situations me semblent isomorphes.
Paul Draper le

3

Je pense qu'un meilleur moyen de subdiviser les choses que les "raisons de changer" est de commencer par se demander s'il serait logique d'exiger que le code qui doit effectuer deux actions (ou plus) ait besoin de contenir une référence d'objet distincte. pour chaque action, et s'il serait utile d'avoir un objet public pouvant effectuer une action mais pas l'autre.

Si les réponses aux deux questions sont oui, cela suggérerait que les actions devraient être effectuées par des classes séparées. Si les réponses aux deux questions sont non, cela suggérerait que du point de vue public, il devrait y avoir une classe; si le code utilisé est difficile à manier, il peut être subdivisé en classe interne en classes privées. Si la réponse à la première question est non, mais que la seconde soit oui, il devrait exister une classe distincte pour chaque action, ainsi qu'une classe composite incluant des références aux instances des autres.

Si vous avez des classes distinctes pour le clavier, le bip, la lecture numérique, l’imprimante et le tiroir-caisse d’une caisse, et aucune classe composite pour une caisse enregistreuse complète, le code censé traiter une transaction peut finir par être invoqué accidentellement dans une Ce mode prend les entrées du clavier d’une machine, produit du bruit provenant du bip de la deuxième machine, affiche les chiffres sur l’affichage de la troisième machine, imprime un reçu sur l’imprimante d’une quatrième machine et ouvre le cinquième tiroir-caisse. Chacune de ces sous-fonctions pourrait être utilement gérée par une classe séparée, mais il devrait également y avoir une classe composite qui les associe. La classe composite doit déléguer autant de logique que possible aux classes constituantes,

On pourrait dire que la "responsabilité" de chaque classe consiste soit à intégrer une logique réelle, soit à fournir un point d'attachement commun à plusieurs autres classes qui le font, mais il est important de se concentrer avant tout sur la manière dont le code client doit afficher une classe. S'il est logique que le code client voie quelque chose comme un seul objet, alors le code client doit le voir comme un seul objet.


C'est un conseil judicieux. Il est peut-être intéressant de souligner que vous divisez les responsabilités en fonction de critères autres que celui du PÉR.
Jørgen Fogh

1
Analogie avec une voiture: je n'ai pas besoin de savoir combien d'essence il y a dans le réservoir de quelqu'un d'autre, ni de vouloir activer les essuie-glaces de quelqu'un d'autre. (mais c'est la définition de l'internet) (Chut! vous allez gâcher l'histoire)

1
@nocomprende - "Je n'ai pas besoin de savoir combien d'essence il y a dans le réservoir de quelqu'un d'autre" - sauf si vous êtes un adolescent qui essaie de décider laquelle des voitures de la famille "emprunter" pour votre prochain voyage ...;)
alephzero

3

SRP est difficile à obtenir. Il s’agit principalement d’attribuer des "emplois" à votre code et de s’assurer que chaque partie a des responsabilités claires. Comme dans la vie réelle, dans certains cas, le partage du travail entre personnes peut s’avérer tout à fait naturel, mais dans d’autres cas, il peut s'avérer très délicat, surtout si vous ne le connaissez pas (ou le travail).

Je te recommande toujours juste écrire du code simple qui fonctionne en premier lieu , puis de le refactoriser un peu: vous aurez tendance à voir comment le code se met naturellement à se regrouper après un certain temps. Je pense que c'est une erreur de forcer les responsabilités avant de connaître le code (ou les personnes) et le travail à effectuer.

Une chose que vous remarquerez est que le module commence à en faire trop et qu’il est difficile à déboguer / maintenir. C'est le moment de refacturer; Quel devrait être le travail principal et quelles tâches pourraient être confiées à un autre module? Par exemple, doit-il gérer les contrôles de sécurité et les autres tâches, ou devez-vous effectuer des contrôles de sécurité ailleurs auparavant, ou cela rendra-t-il le code plus complexe?

Utilisez trop d’indices indirects et tout redevient un gâchis… comme pour d’autres principes, celui-ci sera en conflit avec d’autres, comme KISS, YAGNI, etc. Tout est une question d’équilibre.


Le PÉR n'est-il pas juste la cohésion de Constantine?
Nick Keighley

Vous trouverez naturellement ces modèles si vous codez assez longtemps, mais vous pouvez accélérer l'apprentissage en les nommant et cela facilite la communication ...
Christophe Roussy

@ NickKeighley Je pense que c'est une cohésion, pas tellement écrite, mais vue sous un autre angle.
Sdenham

3

"Principe de responsabilité unique" est peut-être un nom déroutant. "Une seule raison de changer" est une meilleure description du principe, mais il est encore facile de se méprendre. Nous ne parlons pas de dire ce qui cause les objets à changer d'état au moment de l'exécution. Nous discutons de ce qui pourrait amener les développeurs à devoir modifier le code à l'avenir.

Sauf si nous corrigeons un bogue, le changement sera dû à une exigence nouvelle ou modifiée. Vous devrez penser à l'extérieur du code lui-même et imaginer quels facteurs extérieurs pourraient entraîner une modification indépendante des exigences . Dire:

  • Les taux d'imposition changent en raison d'une décision politique.
  • Le marketing décide de changer le nom de tous les produits
  • L'interface utilisateur doit être repensée pour être accessible
  • La base de données est encombrée, vous devez donc procéder à certaines optimisations.
  • Vous devez accueillir une application mobile
  • etc...

Idéalement, vous souhaitez que des facteurs indépendants affectent différentes classes. Par exemple, étant donné que les taux d'imposition changent indépendamment des noms de produits, les modifications ne devraient pas affecter les mêmes classes. Sinon, vous courez le risque de voir une modification de taxe introduire une erreur dans la dénomination du produit, qui est le type de couplage étroit que vous souhaitez éviter avec un système modulaire.

Donc, ne vous concentrez pas uniquement sur ce qui pourrait changer - tout pourrait éventuellement changer dans le futur. Concentrez-vous sur ce qui pourrait changer indépendamment . Les changements sont généralement indépendants s'ils sont causés par différents acteurs.

Votre exemple avec des titres d'emploi est sur la bonne voie, mais vous devriez le prendre plus littéralement! Si le marketing peut entraîner des modifications du code et que les finances peuvent entraîner d'autres modifications, celles-ci ne doivent pas affecter le même code, car elles portent littéralement des intitulés de postes différents et, par conséquent, les modifications sont apportées indépendamment.

Pour citer Oncle Bob qui a inventé le terme:

Lorsque vous écrivez un module logiciel, vous voulez vous assurer que, lorsque des modifications sont demandées, ces modifications ne peuvent émaner que d'une seule personne, ou plutôt d'un seul groupe de personnes étroitement liées représentant une seule fonction métier étroitement définie. Vous souhaitez isoler vos modules des complexités de l'organisation dans son ensemble et concevoir vos systèmes de manière à ce que chaque module soit responsable (réponde) aux besoins de cette seule fonction métier.

Donc, pour résumer: une "responsabilité" est de s'occuper d'une seule fonction commerciale. Si plus d'un acteur peut vous obliger à changer de classe, la classe enfreint probablement ce principe.


Selon son livre "Clean Architecture", c'est tout à fait juste. Les règles métier doivent provenir d'une source et d'une seule source. Cela signifie que les ressources humaines, les opérations et les services informatiques doivent tous coopérer pour formuler les exigences dans une "responsabilité unique". Et c'est le principe. +1
Benny Skogberg

2

Un bon article qui explique les principes de la programmation SOLID et donne des exemples de code respectant et ne respectant pas ces principes est https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- conception .

Dans l'exemple relatif à SRP, il donne un exemple de quelques classes de formes (cercle et carré) et une classe conçue pour calculer la surface totale de plusieurs formes.

Dans son premier exemple, il crée la classe de calcul d'aire et la renvoie en sortie au format HTML. Plus tard, il décide de l’afficher au format JSON et doit changer de classe de calcul de zone.

Le problème de cet exemple est que sa classe de calcul d'aire est responsable du calcul de l'aire des formes ET de l'affichage de cette aire. Il utilise ensuite une autre classe spécialement conçue pour afficher les zones.

Ceci est un exemple simple (et plus facile à comprendre en lisant l'article car il contient des extraits de code), mais illustre l'idée de base de SRP.


0

Tout d’abord, vous avez en fait deux problèmes distincts : le problème des méthodes à mettre dans vos classes et le problème de la surcharge d’interface.

Des interfaces

Vous avez cette interface:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

On peut supposer que vous avez plusieurs classes conformes à l' CustomerCRUDinterface (sinon une interface n'est pas nécessaire), ainsi que certaines fonctions do_crud(customer: CustomerCRUD)qui prennent un objet conforme. Mais vous avez déjà rompu le PÉR: vous avez lié ces quatre opérations distinctes.

Disons que plus tard, vous utiliserez des vues de base de données. Une vue de base de données ne dispose que de la Readméthode disponible. Mais vous voulez écrire une fonction do_query_stuff(customer: ???)qui exploite les opérateurs de manière transparente sur des tables ou des vues complètes; il utilise seulement leRead méthode, après tout.

Alors créez une interface

public Interface CustomerReader {public client lu (customerID: int)}

et factorisez votre CustomerCrudinterface comme:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Mais il n'y a pas de fin en vue. Il peut y avoir des objets que nous pouvons créer mais pas mettre à jour, etc. Ce trou de lapin est trop profond. Le seul moyen sensé de respecter le principe de responsabilité unique est de faire en sorte que toutes vos interfaces contiennent exactement une méthode. . Go suit effectivement cette méthodologie de ce que j'ai vu, avec la grande majorité des interfaces contenant une seule fonction; Si vous souhaitez spécifier une interface contenant deux fonctions, vous devez créer maladroitement une nouvelle interface combinant les deux. Vous obtenez bientôt une explosion combinatoire d'interfaces.

La solution à ce problème consiste à utiliser un sous-typage structurel (implémenté dans, par exemple, OCaml) au lieu d’interfaces (qui sont une forme de sous-typage nominal). Nous ne définissons pas d'interfaces; à la place, nous pouvons simplement écrire une fonction

let do_customer_stuff customer = customer.read ... customer.update ...

cela appelle toutes les méthodes que nous aimons. OCaml utilisera l'inférence de type pour déterminer si un objet implémentant ces méthodes peut être transmis. Dans cet exemple, il serait déterminé que customera type <read: int -> unit, update: int -> unit, ...>.

Des classes

Cela résout le désordre d' interface ; mais nous devons encore implémenter des classes contenant plusieurs méthodes. Par exemple, devrions-nous créer deux classes différentes, CustomerReaderet CustomerWriter? Que se passe-t-il si nous voulons changer la manière dont les tables sont lues (par exemple, nous mettons maintenant en cache nos réponses dans les redis avant de récupérer les données), mais comment sont-elles écrites? Si vous suivez cette chaîne de raisonnement jusqu'à sa conclusion logique, vous êtes entraîné inextricablement à la programmation fonctionnelle :)


4
"Sans signification" est un peu fort. Je pouvais me mettre derrière "mystique" ou "zen". Mais pas tout à fait sans signification!
svidgen

Pouvez-vous expliquer un peu plus pourquoi le sous-typage structurel est une solution?
Robert Harvey

@RobertHarvey a restructuré ma réponse de manière significative
gardenhead

4
J'utilise des interfaces même lorsqu'une seule classe l'implémente. Pourquoi? Se moquer dans les tests unitaires.
Eternal21

0

Dans mon esprit, la chose la plus proche d'un SRP qui me vient à l'esprit est un flux d'utilisation. Si vous ne disposez pas d'un flux d'utilisation clair pour une classe donnée, votre classe a probablement une odeur de design.

Un flux d'utilisation serait une succession d'appels à une méthode donnée qui vous donnerait un résultat attendu (donc vérifiable). En gros, vous définissez une classe avec les cas d’utilisation qu’il a obtenus à mon humble avis, c’est pourquoi toute la méthodologie du programme met l’accent sur les interfaces plutôt que sur la mise en oeuvre.


0

C’est pour parvenir à ce que de multiples changements d’exigences ne nécessitent pas que votre composant change .

Mais bonne chance à comprendre qu'à première vue, lorsque vous entendez parler de SOLID.


Je vois beaucoup de commentaires disant que SRP et YAGNI peuvent se contredire, mais que YAGN I a été mis en application par TDD (GOOS, London School) m'a appris à penser et à concevoir mes composants du point de vue d'un client. J'ai commencé à concevoir mes interfaces avec le minimum que tout client souhaite faire, c'est peu de choses qu'il devrait faire . Et cet exercice peut être fait sans aucune connaissance de TDD.

J'aime la technique décrite par Oncle Bob (malheureusement, je ne me souviens pas d'où), qui se présente comme suit:

Demandez-vous, que fait cette classe?

Est-ce que votre réponse contenait soit Et ou Ou

Si tel est le cas, extrayez cette partie de la réponse, cela relève de votre propre responsabilité.

Cette technique est un absolu et, comme @svidgen l’a dit, le SRP est un jugement, mais quand on apprend quelque chose de nouveau, les absolus sont ce qu’il ya de mieux, il est plus facile de toujours faire quelque chose. Assurez-vous que la raison pour laquelle vous ne vous séparez pas est; une estimation éclairée, et non pas parce que vous ne savez pas comment. C'est l'art, et cela prend de l'expérience.


Je pense que beaucoup de réponses semblent argumenter en faveur du découplage quand on parle de SRP .

SRP est pas pour vous assurer un changement ne se propage pas sur le graphe de dépendance.

Théoriquement, sans SRP , vous n'auriez aucune dépendance ...

Un changement ne devrait pas causer de changement à plusieurs endroits dans l'application, mais nous avons d'autres principes pour cela. SRP améliore cependant le principe de fermeture fermée . Ce principe concerne plus l'abstraction, cependant, les abstractions plus petites sont plus faciles à réimplémenter .

Ainsi, lorsque vous enseignez SOLID dans son ensemble, veillez à préciser que SRP vous permet de modifier moins de code lorsque les exigences changent, alors qu'en réalité, cela vous permet d'écrire moins de nouveau code.


3
When learning something new, absolutes are the best, it is easier to just always do something.- D'après mon expérience, les nouveaux programmeurs sont beaucoup trop dogmatiques. L'absolutisme conduit à des développeurs non-pensants et à une programmation culte du cargo. Dire "faites ceci" est bien, pourvu que vous sachiez que la personne à qui vous parlez devra désapprendre plus tard ce que vous lui avez enseigné.
Robert Harvey

@ RobertHarvey, Tout à fait vrai, cela crée un comportement dogmatique et vous devez désapprendre / réapprendre à mesure que vous gagnez de l'expérience. Ceci est mon point cependant. Si un nouveau programmeur essaie de faire appel à des jugements sans aucun moyen de motiver sa décision, cela semble tout à fait inutile, car ils ne savent pas pourquoi cela a fonctionné, quand cela a fonctionné. En incitant les gens à en faire trop , cela leur apprend à rechercher les exceptions au lieu de faire des suppositions sans réserve. Tout ce que vous avez dit sur l'absolutisme est correct, c'est pourquoi il ne devrait être qu'un point de départ.
Chris Wohlert

@RobertHarvey, Une rapide vie réelle par exemple: Vous pourriez apprendre à vos enfants à toujours être honnête, mais à mesure qu'ils grandissent, ils réaliseront probablement quelques exceptions où les gens ne veulent pas entendre leurs pensées les plus honnêtes. S'attendre à ce qu'un enfant de 5 ans fasse un jugement correct pour être honnête est au mieux optimiste. :)
Chris Wohlert

0

Il n'y a pas de réponse claire à cela. Bien que la question soit étroite, les explications ne le sont pas.

Pour moi, c'est un peu comme le rasoir d'Occam si vous le souhaitez. C'est un idéal où j'essaie de mesurer mon code actuel. Il est difficile de le décrire avec des mots clairs et simples. Une autre métaphore serait "un sujet" aussi abstrait, c'est-à-dire difficile à saisir, que "une seule responsabilité". Une troisième description serait "traitant d'un niveau d'abstraction".

Qu'est-ce que cela signifie concrètement?

Dernièrement, j'utilise un style de codage qui comprend principalement deux phases:

La phase I est décrite comme un chaos créatif. Dans cette phase, j'écris le code pendant que les pensées circulent - c'est-à-dire crues et laides.

La phase II est le contraire. C'est comme nettoyer après un ouragan. Cela prend le plus de travail et de discipline. Et ensuite, je regarde le code du point de vue d'un designer.

Je travaille principalement en Python maintenant, ce qui me permet de penser à des objets et à des classes plus tard. Première phase I - Je n'écris que des fonctions et les diffuse presque aléatoirement dans différents modules. Au cours de la phase II , une fois les travaux en cours, j’examine de plus près quel module traite quelle partie de la solution. Et en parcourant les modules, des sujets me sont apparus. Certaines fonctions sont liées thématiquement. Ce sont de bons candidats pour les cours . Et après avoir transformé les fonctions en classes - ce qui est presque fait avec l'indentation et l'ajout selfà la liste des paramètres en python;) - j'utilise SRPcomme Razor d'Occam pour supprimer les fonctionnalités des autres modules et classes.

Un exemple courant peut être l’ écriture de petites fonctionnalités d’exportation l’autre jour.

Il y avait le besoin de CSV , Excel et des feuilles Excel combinées dans un zip.

La fonctionnalité simple a été réalisée dans trois vues (= fonctions). Chaque fonction utilisait une méthode commune pour déterminer les filtres et une seconde méthode pour récupérer les données. Ensuite, dans chaque fonction, l’exportation a été préparée et a été envoyée en tant que réponse du serveur.

Il y avait trop de niveaux d'abstraction mélangés:

I) traitement des demandes / réponses entrantes / sortantes

II) déterminer les filtres

III) récupération des données

IV) transformation des données

La première étape consistait à utiliser une seule abstraction ( exporter) pour traiter les couches II à IV.

Le seul reste était le sujet traitant des demandes / réponses . Au même niveau d'abstraction, l' extraction des paramètres de requête est satisfaisante. J'ai donc eu pour cette vue une "responsabilité".

Deuxièmement, je devais séparer l'exportateur qui, comme nous l'avons vu, consistait en au moins trois autres couches d'abstraction.

La détermination des critères de filtrage et la remontée effective se situent presque au même niveau d'abstraction (les filtres sont nécessaires pour obtenir le bon sous-ensemble de données). Ces niveaux ont été placés dans quelque chose comme une couche d'accès aux données .

Dans l'étape suivante, j'ai cassé les mécanismes d'exportation actuels: lorsqu'il était nécessaire d'écrire dans un fichier temporel, je l'ai divisé en deux "responsabilités": une pour l'écriture proprement dite des données sur disque et une autre partie concernant le format réel.

Lors de la formation des classes et des modules, les choses sont devenues plus claires, ce qui appartenait à où. Et toujours la question latente, si la classe en fait trop .

Comment déterminez-vous les responsabilités que chaque classe devrait avoir et comment définissez-vous une responsabilité dans le contexte du PÉR?

Il est difficile de donner une recette à suivre. Bien sûr, je pourrais répéter la règle «un niveau d’abstraction» cryptée si cela aide.

C'est surtout pour moi une sorte d '"intuition artistique" qui mène au design actuel; Je modélise le code comme un artiste peut sculpter de l'argile ou peindre.

Imagine-moi comme un codeur Bob Ross ;)


0

Ce que j'essaie de faire pour écrire du code qui suit le SRP:

  • Choisissez un problème spécifique que vous devez résoudre;
  • Écrivez le code qui le résout, écrivez tout dans une méthode (par exemple: main);
  • Analysez soigneusement le code et essayez, en fonction de l'entreprise, de définir les responsabilités visibles dans toutes les opérations effectuées (il s'agit de la partie subjective qui dépend également de l'entreprise / du projet / du client);
  • Veuillez noter que toutes les fonctionnalités sont déjà implémentées. la prochaine étape est uniquement l’organisation du code (aucune fonctionnalité ou mécanisme supplémentaire ne sera désormais implémenté dans cette approche);
  • En fonction des responsabilités que vous avez définies dans les étapes précédentes (définies en fonction de l'activité et de l'idée "une raison de changer"), extrayez une classe ou une méthode distincte pour chacune d'elles;
  • Veuillez noter que cette approche ne concerne que le SPR; idéalement, il devrait y avoir des étapes supplémentaires ici pour essayer de respecter également les autres principes.

Exemple:

Problème: obtenez deux nombres de l'utilisateur, calculez leur somme et envoyez le résultat à l'utilisateur:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

Ensuite, essayez de définir les responsabilités en fonction des tâches à exécuter. Extrayez les classes appropriées:

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

Ensuite, le programme refactorisé devient:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

Remarque: cet exemple très simple ne prend en compte que le principe SRP. L'utilisation des autres principes (par exemple: le code "L" devrait dépendre d'abstractions plutôt que de concrétions) procurerait plus d'avantages au code et le rendrait plus facile à gérer en cas de changement d'activité.


1
Votre exemple est trop simple pour illustrer correctement SRP. Personne ne ferait cela dans la vraie vie.
Robert Harvey

Oui, dans les projets réels, j'écris un pseudo-code plutôt que d'écrire le code exact, comme dans mon exemple. Après le pseudocode, j'essaie de répartir les responsabilités comme je le faisais dans l'exemple. En tout cas, c'est comme ça que je le fais.
Emerson Cardoso

0

Extrait du livre de Robert C. Martins, Clean Architecture: Le Guide de l'artisan de la structure et de la conception de logiciels , publié le 10 septembre 2017, Robert écrit à la page 62 ce qui suit:

Historiquement, le SRP a été décrit comme suit:

Un module doit avoir une et une seule raison de changer

Les systèmes logiciels sont modifiés pour satisfaire les utilisateurs et les parties prenantes; ces utilisateurs et parties prenantes sont la "raison de changer". que le principe parle. En effet, nous pouvons reformuler le principe en ces termes:

Un module devrait être responsable devant un, et seulement un, utilisateur ou acteur

Malheureusement, les mots "utilisateur" et "partie prenante" ne sont pas vraiment le bon mot à utiliser ici. Il y aura probablement plus d'un utilisateur ou intervenant qui souhaite que le système soit modifié de manière saine. Au lieu de cela, nous nous référons vraiment à un groupe - une ou plusieurs personnes qui ont besoin de ce changement. Nous allons parler de ce groupe en tant qu'acteur .

Ainsi, la version finale du SRP est:

Un module devrait être responsable devant un et un seul acteur.

Donc, il ne s'agit pas de code. Le SRP consiste à contrôler le flux des exigences et des besoins de l'entreprise, qui ne peuvent provenir que d'une seule source.


Je ne sais pas pourquoi vous faites la distinction "qu'il ne s'agit pas de code." Bien sûr, il s'agit de code. c'est le développement de logiciels.
Robert Harvey

@ RobertHarvey Mon point est que le flux des exigences provient d'une source, l'acteur. Les utilisateurs et les parties prenantes ne sont pas dans le code, ils sont dans les règles métier qui nous sont soumises comme exigences. La SRP est donc un processus permettant de contrôler ces exigences, ce qui pour moi n’est pas du code. C'est du développement logiciel (!), Mais pas du code.
Benny Skogberg
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.