Quelles sont simplement les techniques pratiques que les gens utilisent pour vérifier si une classe viole le principe de responsabilité unique?
Je sais qu'une classe ne devrait avoir qu'une seule raison de changer, mais cette phrase manque quelque peu d'un moyen pratique de vraiment mettre cela en œuvre.
La seule façon que j'ai trouvée est d'utiliser la phrase "Le ......... devrait ......... lui-même." où le premier espace est le nom de la classe et le dernier est le nom de la méthode (responsabilité).
Cependant, il est parfois difficile de déterminer si une responsabilité viole vraiment le SRP.
Existe-t-il d'autres moyens de vérifier le SRP?
Remarque:
La question n'est pas de savoir ce que signifie le SRP, mais plutôt une méthodologie pratique ou une série d'étapes pour vérifier et mettre en œuvre le SRP.
MISE À JOUR
J'ai ajouté un exemple de classe qui viole clairement le SRP. Ce serait formidable si les gens pouvaient l'utiliser comme exemple pour expliquer comment ils abordent le principe de la responsabilité unique.
L'exemple vient d' ici .
Réponses:
Le SRP déclare, en termes non équivoques, qu'une classe ne devrait avoir qu'une seule raison de changer.
Déconstruisant la classe "report" dans la question, elle a trois méthodes:
printReport
getReportData
formatReport
En ignorant la redondance
Report
utilisée dans chaque méthode, il est facile de voir pourquoi cela viole le SRP:Le terme "impression" implique une sorte d'interface utilisateur ou une imprimante réelle. Cette classe contient donc une certaine quantité d'interface utilisateur ou de logique de présentation. Une modification des exigences de l'interface utilisateur nécessitera une modification de la
Report
classe.Le terme "données" implique une structure de données quelconque, mais ne précise pas vraiment quoi (XML? JSON? CSV?). Quoi qu'il en soit, si le «contenu» du rapport change un jour, cette méthode changera également. Il y a couplage à une base de données ou à un domaine.
formatReport
est juste un nom terrible pour une méthode en général, mais je suppose en le regardant qu'elle a encore une fois quelque chose à voir avec l'interface utilisateur, et probablement un aspect différent de l'interface utilisateurprintReport
. Donc, une autre raison sans rapport avec le changement.Donc, cette seule classe est éventuellement couplée à une base de données, un périphérique écran / imprimante et une logique de formatage interne pour les journaux ou la sortie de fichiers, etc. En regroupant les trois fonctions dans une même classe, vous multipliez le nombre de dépendances et triplez la probabilité que tout changement de dépendance ou d'exigence casse cette classe (ou autre chose qui en dépend).
Une partie du problème ici est que vous avez choisi un exemple particulièrement épineux. Vous ne devriez probablement pas avoir de classe appelée
Report
, même si cela ne fait qu'une chose , car ... quel rapport? Tous les «rapports» ne sont-ils pas tous des bêtes complètement différentes, basées sur des données différentes et des exigences différentes? Et un rapport n'est-il pas quelque chose qui a déjà été formaté, que ce soit pour l'écran ou pour l'impression?Mais, en regardant au-delà de cela et en créant un nom concret hypothétique - appelons-le
IncomeStatement
(un rapport très courant) - une architecture "SRP" appropriée aurait trois types:IncomeStatement
- le domaine et / ou la classe de modèle qui contient et / ou calcule les informations qui apparaissent sur les rapports formatés.IncomeStatementPrinter
, qui implémenterait probablement une interface standard commeIPrintable<T>
. A une méthode cléPrint(IncomeStatement)
, et peut-être d'autres méthodes ou propriétés pour configurer les paramètres spécifiques à l'impression.IncomeStatementRenderer
, qui gère le rendu d'écran et est très similaire à la classe d'imprimante.Vous pourriez également éventuellement ajouter des classes plus spécifiques aux fonctionnalités comme
IncomeStatementExporter
/IExportable<TReport, TFormat>
.Cela est rendu beaucoup plus facile dans les langages modernes avec l'introduction de génériques et de conteneurs IoC. La plupart de votre code d'application n'a pas besoin de s'appuyer sur la
IncomeStatementPrinter
classe spécifique , il peut utiliserIPrintable<T>
et donc fonctionner sur tout type de rapport imprimable, ce qui vous donne tous les avantages perçus d'uneReport
classe de base avec uneprint
méthode et aucune des violations SRP habituelles . L'implémentation réelle ne doit être déclarée qu'une seule fois, dans l'enregistrement du conteneur IoC.Certaines personnes, confrontées à la conception ci-dessus, répondent par quelque chose comme: "mais cela ressemble à du code procédural, et le but de la POO était de nous éloigner - de la séparation des données et du comportement!" À quoi je dis: mal .
Ce
IncomeStatement
n'est pas seulement des "données", et l'erreur mentionnée ci-dessus est ce qui fait que beaucoup de gens OOP sentent qu'ils font quelque chose de mal en créant une classe aussi "transparente" et par la suite commencent à brouiller toutes sortes de fonctionnalités non liées dans leIncomeStatement
(enfin, que et paresse générale). Cette classe peut commencer comme de simples données mais, avec le temps, c'est garanti, elle finira comme un modèle .Par exemple, un état des revenus réels comprend les revenus totaux , les dépenses totales et les lignes de revenus nets . Un système financier bien conçu ne les stockera probablement pas car il ne s'agit pas de données transactionnelles - en fait, elles changent en fonction de l'ajout de nouvelles données transactionnelles. Cependant, le calcul de ces lignes sera toujours exactement le même, que vous imprimiez, rendiez ou exportiez le rapport. Ainsi , votre
IncomeStatement
classe va avoir une quantité juste de comportement sous la forme degetTotalRevenues()
,getTotalExpenses()
et desgetNetIncome()
méthodes, et probablement plusieurs autres. C'est un véritable objet de style OOP avec son propre comportement, même s'il ne semble pas vraiment "faire" beaucoup.Mais les méthodes
format
etprint
, elles n'ont rien à voir avec les informations elles-mêmes. En fait, il n'est pas trop improbable que vous souhaitiez avoir plusieurs implémentations de ces méthodes, par exemple une déclaration détaillée pour la direction et une déclaration moins détaillée pour les actionnaires. La séparation de ces fonctions indépendantes dans différentes classes vous donne la possibilité de choisir différentes implémentations au moment de l'exécution sans la charge d'uneprint(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
méthode universelle. Beurk!J'espère que vous pourrez voir où la méthode ci-dessus, massivement paramétrée, va mal, et où les implémentations distinctes vont bien; dans le cas d'un seul objet, chaque fois que vous ajoutez une nouvelle ride à la logique d'impression, vous devez changer votre modèle de domaine ( Tim in finance veut des numéros de page, mais uniquement sur le rapport interne, pouvez-vous ajouter cela? ) par opposition à en ajoutant simplement une propriété de configuration à une ou deux classes satellites à la place.
L'implémentation correcte du SRP consiste à gérer les dépendances . En un mot, si une classe fait déjà quelque chose d'utile et que vous envisagez d'ajouter une autre méthode qui introduirait une nouvelle dépendance (comme une interface utilisateur, une imprimante, un réseau, un fichier, peu importe), ne le faites pas . Pensez à la façon dont vous pourriez ajouter cette fonctionnalité dans une nouvelle classe à la place, et comment vous pourriez faire en sorte que cette nouvelle classe s'intègre dans votre architecture globale (c'est assez facile lorsque vous concevez l'injection de dépendances). C'est le principe / processus général.
Note latérale: Comme Robert, je rejette manifestement l'idée qu'une classe conforme à SRP ne devrait avoir qu'une ou deux variables d'état. On pourrait rarement s'attendre à ce qu'une telle enveloppe mince fasse quelque chose de vraiment utile. Alors n'allez pas trop loin avec ça.
la source
IncomeStatement
. La conception que vous proposez signifie-t-elle que leIncomeStatement
aura des instances deIncomeStatementPrinter
& deIncomeStatementRenderer
sorte que lorsque j'appelleraiprint()
,IncomeStatement
il déléguera l'appel à laIncomeStatementPrinter
place?IncomeStatement
classe n'a pas uneprint
méthode ou uneformat
méthode, ou toute autre méthode qui ne traite pas directement avec l' inspection ou de manipuler les données du rapport lui - même. C'est à ça que servent ces autres classes. Si vous voulez en imprimer un, vous prenez alors une dépendance à l'IPrintable<IncomeStatement>
interface qui est enregistrée dans le conteneur.Printer
instance dans laIncomeStatement
classe? la façon dont je l'imagine quand je l'appelleIncomeStatement.print()
le délégueraIncomeStatementPrinter.print(this, format)
. Quel est le problème avec cette approche? ... Une autre question, Vous avez mentionné queIncomeStatement
devrait contenir les informations qui apparaissent sur les rapports formatés si je veux qu'elles soient lues à partir de la base de données ou d'un fichier XML, dois-je extraire la méthode qui charge les données dans une classe distincte et lui déléguer l'appelIncomeStatement
?IncomeStatementPrinter
selonIncomeStatement
etIncomeStatement
selonIncomeStatementPrinter
. C'est une dépendance cyclique. Et c'est juste une mauvaise conception; il n'y a aucune raisonIncomeStatement
de savoir quoi que ce soit sur unPrinter
ouIncomeStatementPrinter
- c'est un modèle de domaine, cela ne concerne pas l'impression, et la délégation est inutile car toute autre classe peut créer ou acquérir unIncomeStatementPrinter
. Il n'y a aucune bonne raison d'avoir une notion d'impression dans le modèle de domaine.IncomeStatement
depuis la base de données (ou fichier XML) - généralement, cela est géré par un référentiel et / ou un mappeur, pas le domaine, et encore une fois, vous ne déléguez pas cela dans le domaine; si une autre classe a besoin de lire l' un de ces modèles, elle demande explicitement ce référentiel . À moins que vous n'implémentiez le modèle Active Record, je suppose, mais je ne suis vraiment pas un fan.La façon dont je vérifie le SRP est de vérifier chaque méthode (responsabilité) d'une classe et de poser la question suivante:
"Aurai-je besoin de changer la façon dont j'implémenter cette fonction?"
Si je trouve une fonction que j'aurai besoin d'implémenter de différentes manières (selon une sorte de configuration ou de condition), alors je sais avec certitude que j'ai besoin d'une classe supplémentaire pour gérer cette responsabilité.
la source
Voici une citation de la règle 8 de la callisthénie des objets :
Compte tenu de cette vue (quelque peu idéaliste), on pourrait dire que toute classe qui ne contient qu'une ou deux variables d'état ne risque pas de violer SRP. Vous pouvez également dire que toute classe qui contient plus de deux variables d'état peut violer SRP.
la source
Une implémentation possible (en Java). J'ai pris des libertés avec les types de retour mais dans l'ensemble je pense que cela répond à la question. TBH Je ne pense pas que l'interface avec la classe Report soit si mauvaise, bien qu'un meilleur nom puisse être en ordre. J'ai laissé de côté les déclarations et les assertions de la garde par souci de concision.
EDIT: Notez également que la classe est immuable. Donc, une fois créé, vous ne pouvez rien changer. Vous pouvez ajouter un setFormatter () et un setPrinter () et ne pas avoir trop de problèmes. La clé, à mon humble avis, est de ne pas modifier les données brutes après l'instanciation.
la source
if (reportData == null)
je suppose que vous voulez dire à ladata
place. Deuxièmement, j'espérais savoir comment vous en êtes arrivé à cette mise en œuvre. Comme pourquoi avez-vous décidé de déléguer tous les appels à d'autres objets à la place. Encore une chose sur laquelle je me suis toujours posé la question: est-ce vraiment la responsabilité d'un rapport de s'imprimer?! Pourquoi n'avez-vous pas créé uneprinter
classe distincte qui prend unreport
dans son constructeur?Printer
classe qui prend un rapport ou uneReport
classe qui prend une imprimante? J'ai rencontré un problème similaire avant où je devais analyser un rapport et je me suis disputé avec mon TL si nous devions créer un analyseur qui prend un rapport ou si le rapport devrait avoir un analyseur à l'intérieur et l'parse()
appel lui est délégué.Dans votre exemple, il n'est pas clair que le SRP est violé. Peut-être que le rapport devrait pouvoir se mettre en forme et s’imprimer, s’ils sont relativement simples:
Les méthodes sont si simples , il n'a pas de sens d'avoir
ReportFormatter
ouReportPrinter
classes. Le seul problème flagrant dans l'interface estgetReportData
parce qu'il viole ask don't tell sur un objet sans valeur.D'un autre côté, si les méthodes sont très compliquées ou qu'il existe de nombreuses façons de formater ou d'imprimer un,
Report
il est logique de déléguer la responsabilité (également plus testable):SRP est un principe de conception et non un concept philosophique et il est donc basé sur le code réel avec lequel vous travaillez. Sémantiquement, vous pouvez diviser ou regrouper une classe en autant de responsabilités que vous le souhaitez. Cependant, en tant que principe pratique, SRP devrait vous aider à trouver le code que vous devez modifier . Les signes que vous violez SRP sont:
Vous pouvez résoudre ces problèmes en refactorisant en améliorant les noms, en regroupant du code similaire, en éliminant la duplication, en utilisant une conception en couches et en divisant / combinant les classes selon les besoins. La meilleure façon d'apprendre la SRP est de plonger dans une base de code et de refactoriser la douleur.
la source
Printer
classe qui prend un rapport ou uneReport
classe qui prend une imprimante? Plusieurs fois, je suis confronté à une telle question de conception avant de déterminer si le code se révélera complexe ou non.Le principe de responsabilité unique est étroitement lié à la notion de cohésion . Afin d'avoir une classe hautement cohésive, vous devez avoir une co-dépendance entre les variables d'instance de la classe et ses méthodes; c'est-à-dire que chacune des méthodes doit manipuler autant de variables d'instance que possible. Plus une méthode utilise de variables, plus sa classe est cohérente; une cohésion maximale est généralement impossible.
De plus, pour bien appliquer la SRP, vous comprenez bien le domaine de la logique métier; pour savoir ce que chaque abstraction doit faire. L'architecture en couches est également liée à SRP, en faisant en sorte que chaque couche fasse une chose spécifique (la couche source de données doit fournir des données, etc.).
Pour en revenir à la cohésion même si vos méthodes n'utilisent pas toutes les variables, elles doivent être couplées:
Vous ne devriez pas avoir quelque chose comme le code ci-dessous, où une partie des variables d'instance sont utilisées dans une partie des méthodes, et l'autre partie des variables sont utilisées dans l'autre partie des méthodes (ici, vous devriez avoir deux classes pour chaque partie des variables).
la source