Introduction
Robert C. Martin, le père de SOLID, introduit le Single Responsibility Principle (SRP) en ces termes:
“There should never be more than one reason for a class to change.”
“Il ne devrait jamais y avoir plus d’une raison de changer une classe.”
Avant d’aller plus loin nous nous devons de définir quelques notions:
- responsabilité : on désigne par “responsabilité” une tâche, un rôle attribué à une classe. Si, par exemple, une classe FootballMatch gère à la fois le score du match et le chronomètre on dit qu’elle a deux responsabilités. Robert C. Martin parle de “reason for change”, une raison de changer la classe. Dans notre exemple on voit facilement que l’on devra changer la classe si la durée d’un match change, mais aussi si le comptage des points est modifié. Ces deux choses étant a priori sans rapport, on peut être certain que la classe possède deux responsabilités.
- couplage : le couplage désigne l’inter-dépendance de plusieurs classes. Il existe plusieurs types de couplage, plus ou moins sévère, suivant que l’inter-dépendance se situe au niveau des données, de la communication entre objet ou au niveau de l’implémentation à proprement parler. Ainsi le couplage le plus sévère (certains le nomment “couplage pathologique”) a lieu lorsqu’une classe dépend des détails de l’implémentation d’une autre classe.
- cohésion : on désigne par “cohésion” le fait que l’ensemble des méthodes d’une classe semblent poursuivre un but commun, une même responsabilité, et sont en relation les unes avec les autres.
L’enjeu du SRP est d’augmenter la cohésion et de diminuer le couplage, facilitant ainsi la maintenance du code. En effet, si une classe traite plusieurs aspects différents, c’est à dire exerce plusieurs responsabilités, un changement dans l’un de ces aspects pourrait casser le fonctionnement d’une autre partie de la classe (manque de cohésion) et causer le dysfonctionnement d’autres éléments sans rapport avec l’aspect modifié (couplage). Il peut ainsi être nécessaire de modifier et de recompiler des parties de l’application n’ayant aucun rapport avec la modification.
Cette classe a-t-elle trop de responsabilités?
Lorsque nous avons défini la notion de responsabilité, nous avons parlé de “motif de changement”. En pratique, il n’est pas forcément aisé d’identifier les responsabilités d’une classe.
D’une manière générale, plus la cohésion de la classe est faible et plus il est facile d’identifier les responsabilités. Martin cite l’exemple d’une classe Rectangle qui posséderait à la fois des méthodes permettant de calculer son périmètre et sa surface, et des méthodes liées à l’affichage du rectangle dans un environnement graphique donné. Les deux groupes de fonctions sont clairement distincts et les problèmes de couplage induits par l’usage d’une telle classe sont assez évidents. Ainsi, une application de calcul géométrique en ligne de commande se devrait d’inclure la librairie graphique utilisée par la classe Rectangle, et un changement au sein de cette librairie pourrait faire que l’on doive recompiler et re-tester l’application de calcul géométrique.
Voici quelques moyens d’identifier les responsabilités d’une classe existante:
- la classe a plus d’une raison de changer. Le cas d’école, la base du principe. Dans le cas de notre exemple FootballMatch cela est nettement suffisant.
- la moitié de la classe est appelée uniquement par une partie du programme, et l’autre moitié par une autre partie sans rapport direct. C’est généralement le cas pour des rôles comme la création, la validation et la persistance.
- une partie de la classe dépend d’un élément externe (librairie, base de donnée, interaction utilisateur…) et une autre partie non. Par exemple seules trois méthodes utilisent la librairie d’accès à la base de données, ou seules deux méthodes utilisent la bibliothèque graphique.
- utiliser la classe implique des dépendances “inattendues”. Un code smell : dans l’exemple du Rectangle une dépendance à la bibliothèque graphique est induite dans l’application de calcul géométrique, bizarre…
- on distingue clairement plusieurs groupes de méthodes (nom similaire ou champ lexical commun, données utilisées, fonction appelée commune…). Par exemple une classe aurait les méthodes validateName, validateDate, etc. et d’autres méthodescreateFromFile, createFromString…
Comment appliquer le SRP…avant l’implémentation?
Comme pour tous les principes de SOLID, il est plus simple d’applique le SRP au moment de designer l’architecture de l’application qu’au moment de refactorer.
La façon la plus efficace de séparer les responsabilités identifiées en amont est de les séparer au niveau de l’abstraction, c’est à dire de définir des interfaces différentes pour chacun des rôles identifiés. Dans notre exemple de la classe FootballMatch, nous pouvons créer deux interfacesScoreTracker et TimeManager qui séparent les méthodes dédiées au score de celles liées au chronomètre. On manipulera ensuite ces interfaces de manière séparée. Nous reviendrons plus en détail sur cet aspect dans “SOLID Partie IV : Interface Segregation Principle”
Comment appliquer le SRP…au moment de refactorer?
Il peut être difficile de refactorer une classe présentant trop de responsabilités, en particulier s’il s’agit d’un God Object ou que le code est un Plat de Spaghetti.
Voici quelques techniques:
- Tout d’abord, s’il est possible de séparer les responsabilités au niveau de l’abstraction (voir paragraphe précédent), cela reste la méthode de prédilection, même si le contexte nécessite qu’une même classe implémente toutes les interfaces. Cela nous permet de séparer les concepts pour le reste de l’application.
- Certains langages permettent la création de classes partielles (C# ou Ruby par exemple). Cela permet de définir une même classe par partie, et éventuellement de n’inclure que la partie qui nous intéresse, évitant le couplage inutile. C’est un peu comme si plusieurs classes du même nom étaient chacune assignée à un rôle donné.
- Une responsabilité liée à la persistance des instances peut probablement être déplacée dans un DAO
- Une responsabilité liée à la création des instances peut probablement être déplacée dans un Factory
- Un God Object peut parfois être transformé en Chain Of Responsibility
Conclusion
La difficulté principale de l’application du Single Responsibility Principle se trouve dans l’identification des responsabilités. Cette identification est le point important qui mène à SOLIDifier un code. Nous verrons cela en détail dans “SOLID Partie IV : Interface Segregation Principle” et “SOLID Partie V : Dependency Inversion Principle”. Une fois les responsabilités clairement identifiées, il est assez aisé de construire un code respectant le SRP, notamment en séparant les responsabilités au niveau de l’abstraction, puis de l’implémentation.
Pour aller plus loin
- Certaines responsabilités sont transversales à l’application. On peut s’orienter vers la programmation orientée aspect.
- http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, Robert C. Martin
2 thoughts on “SOLID Partie I : Single Responsibility Principle”