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 utilisateur printReport
. 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 comme IPrintable<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 utiliser IPrintable<T>
et donc fonctionner sur tout type de rapport imprimable, ce qui vous donne tous les avantages perçus d'une Report
classe de base avec une print
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 le IncomeStatement
(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 de getTotalRevenues()
, getTotalExpenses()
et des getNetIncome()
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
et print
, 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'une print(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.