5 exemples faciles pour comprendre les principes SOLID
Introduction
Comme vous savez, un logiciel peut être confectionné de plusieurs façons différentes. En fait, une même fonctionnalité peut être codée sous des designs complètement différents et c’est exactement la source du problème qui fait que les projets de développement ratent souvent leur cible en terme d’estimation d’efforts. En effet, bien qu’il y ait une architecture prédéterminée, le développeur est souvent laissé à lui-même lorsque vient le temps d’implémenter la fonctionnalité. Pour faire un comparaison avec le monde de la construction, c’est un peu comme si on laissait le menuisier choisir comment monter les murs et arranger les pièces… vous imaginez le bordel dans une équipe de 10 menuisiers qui travaillent sur une même maison? Heureusement pour eux (et pour nous!), ils ont des standards à respecter. Pour notre part, nous avons les patrons de conceptions et les principes de programmation, ces derniers étant le sujet de cet article.
Les principes SOLID
L’acronyme SOLID a été inventé par Michael Feathers, blogueur et auteur du livre Working Effectively With Legacy Code, mais a été popularisé par Robert C. Martin que, j’espère, vous connaissez déjà (Clean Code, Clean Coder, etc.).
Ces principes de programmation sont la base de tout code qui se veut clair, propre, facilement maintenable et facile à faire évoluer. Lorsqu’on parle « facilité » de maintenance ou d’évolution à propos du code, il faut comprendre que cela signifie que le coût nécessaire pour effectuer un changement à l’application devrait toujours être inférieur aux bénéfices directement apportés par le changement. Je vous propose donc ici des outils pour vous aider à produire ce type de code.
Single Responsibility Principle (SRP)
Une classe ne devrait avoir qu’une seule raison de changer.
Le SRP ou principe de la responsabilité unique est probablement le plus connu et le plus simple à comprendre, puisqu’il est étroitement lié à la cohésion. Le SRP est aux principes orientés-objet ce que le Singleton est aux patrons de conception. En gros, pour savoir si une classe respecte le SRP, il faut dire: « La classe X fait … » en étant le plus spécifique possible. Si la phrase ci-dessus contient des et ou des ou, alors votre classe a plus d’une responsabilité. De façon plus subtile, si elle contient des mots génériques comme gère ou objet (exemple: gère les utilisateurs, valide les objets), alors vous devriez avoir la puce à l’oreille.
Par contre, il ne faut pas pousser le concept trop loin. Par exemple, une classe qui formate le numéro de téléphone d’un utilisateur peut être un peu trop spécifique.
Exemple
Cette classe encapsule les données d’une utilisateur:
1 2 3 4 5 |
public class User { public String firstName; public String lastName; public String phoneNumber; } |
Cette classe encapsule les données d’une utilisateur et valide les informations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.ezoqc.blog.solid.srp; public class User { private String firstName; private String lastName; private String phoneNumber; public void setFirstName(String firstName) { if (firstName == null) throw new Exception("First name is mandatory"); this.firstName = firstName; } public void setLastName(String lastName) { if (lastName == null) throw new Exception("Last name is mandatory"); this.lastName = lastName; } public void setPhoneNumber(String phoneNumber) { if (phoneNumber == null) throw new Exception("Phone number is mandatory"); if (phoneNumber.length() != 10) throw new Exception("Phone number is not valid"); this.phoneNumber = phoneNumber; } // getters... } |
Open/Closed Principle (OCP)
Les entités d’un logiciel devraient être fermées aux modifications mais ouvertes à l’extension.
L’OCP ou principe ouvert/fermé est beaucoup moins compris que le premier. Lorsqu’on dit qu’une classe est fermée aux modifications et ouverte à l’extension, il faut comprendre qu’il est préférable de créer une sous-classe ou d’ajouter des membres pour lui apporter une modification d’état ou de comportement que de modifier les membres existants.
Donc, si vous devez modifier le corps d’une méthode, sa signature ou encore le type d’une propriété d’une classe, vous avez de très forte chance d’être en train de briser l’OCP.
Exemple
Voici une classe qui applique des validations sur l’âge d’un utilisateur:
1 2 3 4 5 6 7 |
package com.ezoqc.blog.solid.ocp; public class UserAgeValidator { public boolean isOldEnoughToDrinkAlcohol(int age) { return age >= 18; } } |
Si nous voulons ajouter de l’information concernant la province et qu’on ne respecte pas l’OCP, on peut arriver avec:
1 2 3 4 5 6 7 8 |
package com.ezoqc.blog.solid.ocp; public class UserAgeValidator { public boolean isOldEnoughToDrinkAlcohol(int age, String provinceCode) { return provinceCode.equalsIgnoreCase('qc') && age >= 18 || provinceCode.equalsIgnoreCase('on') && age >= 21 } } |
Ce qui est mal avec cet exemple, c’est que nous avons modifié la signature de la fonction, alors nous aurons assurément des problèmes à la compilation. Au contraire, si on veut respecter l’OCP, on y aurait été avec une solution plus élégante:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.ezoqc.blog.solid.ocp; public interface UserAgeValidator { boolean isOldEnoughToDrinkAlcohol(int age); } public class QuebecAgeValidator implements UserAgeValidator { public boolean isOldEnoughToDrinkAlcohol(int age) { return age >= 18; } } public class OntarioAgeValidator implements UserAgeValidator { public boolean isOldEnoughToDrinkAlcohol(int age) { return age >= 21; } } |
Liskov Substitution Principle (LSP)
Si B et C sont des implémentations de A, alors B et C doivent pouvoir être inter-changées sans affecter l’exécution du programme.
L’LSP ou principe de substitution de Liskov est très simple, mais son nom fait un peu peur. En effet, il faut comprendre que les classes partageant une même classe parente partagent aussi un état et/ou un comportement. Si un comportement est défini dans une classe et qu’une autre en hérite, alors elle doit se conformer. Si ce comportement n’est pas désiré dans la classe, alors le LSP n’est pas respecté.
Exemple
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class User { private String emailAddress; public String getEmailAddress() { return this.emailAddress; } } public class AnonymousUser extends User { public String getEmailAddress() { throw new RuntimeException("Anonymous users don't have an email address."); } } |
Ici, le fait de lancer une exception à l’appel d’une méthode est un indicatif. Il va de même pour les méthodes héritées d’une interface ou d’une classe abstraite qui ne ferait que lancer une exception. En général, c’est un signe que le modèle de données a un problème. Il est vrai qu’un utilisateur anonyme est un utilisateur, mais il est faux que tous les utilisateurs ont des adresses courriel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.ezoqc.blog.solid.lsp; public abstract class User { } public class RegisteredUser extends User{ private String emailAddress; public String getEmailAddress() { return this.emailAddress; } } public class AnonymousUser extends User { } |
Interface Segregation Principle (ISP)
L’appelant ne devrait pas connaître les méthodes qu’il n’a pas à utiliser.
L’ISP ou principe de ségrégation de l’interface est un autre principe nébuleux que nous allons démystifier. Le couplage est un principe orienté-objet qui se quantifie en déterminant les cas d’utilisation des classes. On dit de deux classes qu’elles ont un couplage fort lorsqu’elles s’utilisent. Afin de diminuer le couplage, il est possible de dépendre d’une abstraction, cachant ainsi l’implémentation à l’appelant. Le problème avec l’ISP, c’est lorsqu’une interface ou une classe abstraite devient trop volumineuse et expose trop d’information via son API.
On peut comprendre que l’ISP est lié avec le SRP par le fait que l’interface peut définir plusieurs concepts qui ne sont pas nécessairement liés.
Exemple
Dans l’exemple ci-dessous, la classe Vehicle a deux comportements. Nous pouvons démarrer la voiture ou allumer les lumières.
1 2 3 4 5 6 7 8 |
public class Vehicle { public void startEngine() { System.out.println("Engine started"); } public void turnLightsOn() { System.out.println("Lights turned on"); } } |
En supposant qu’une deuxième classe aurait la responsabilité d’allumer les lumières comme suit:
1 2 3 4 5 6 7 8 |
class VehicleLightTurner { public Vehicle vehicle; public void turnOnTheLights() { vehicle.turnLightsOn(); } } |
Afin de respecter au maximum l’ISP, il faut se demande pourquoi une classe responsable d’allumer les lumières devrait savoir qu’un véhicule peut être démarrer. On peut alors extraire l’interface afin que notre classe ne connaisse que ce dont elle a besoin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public interface VehicleWithLights { void turnLightsOn(); } public class Vehicle implements VehicleWithLights { public void startEngine() { System.out.println("Engine started"); } public void turnLightsOn() { System.out.println("Lights turned on"); } } class VehicleLightTurner { public VehicleWithLights vehicle; public void turnOnTheLights() { vehicle.turnLightsOn(); } } |
Dependency Inversion Principle (DIP)
Les modules d’une application devraient dépendre d’abstractions.
Le DIP ou principe d’inversion de dépendance nous dit que les dépendances d’une classe ne devraient jamais être concrètes. Puisqu’elle ne doit pas connaître l’implémentation de ses dépendances, nous pouvons nous assurer du respect de ce principe en implémentant le patron de conception d’injection de dépendances, soit via le constructeur, soit via les mutateurs.
Supposons que vous avez une classe responsable de la journalisation. Cette classe est utilisée dans un service afin de journaliser les entrées et sorties:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Logger { public void log(String msg) { System.out.println(msg); } } class SomeService { private Logger logger; public SomeService() { this.logger = new Logger(); } public void someMethod() { this.logger.log("Hi!"); } } |
Ici, Logger
et SomeService
sont fortement couplées. Le problème surgira lorsqu’il faudra journaliser dans un fichier au lieu de la console, surtout si Logger
est utilisée comme telle partout dans l’application. Par contre, si nous dépendons d’une interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ConsoleLogger implements Logger { public void log(String msg) { System.out.println(msg); } } class SomeService { private Logger logger; public SomeService() { this.logger = new ConsoleLogger(); } public void someMethod() { this.logger.log("Hi!"); } } |
Malheureusement, nos classes restent encore couplées. Pour sortir l’implémentation complètement, on doit inverser les dépendances en implémentant l’injection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
interface Logger { void log(String msg); } class ConsoleLogger implements Logger { public void log(String msg) { System.out.println(msg); } } class SomeService { private Logger logger; public SomeService(Logger logger) { this.logger = logger; } public void someMethod() { this.logger.log("Hi!"); } } |
Et voilà! SomeService
n’a plus connaissance de la technologie utilisée pour la journalisation.
Conclusion
Les principes SOLID sont un outil puissant pour un développeur s’il prend le temps de les comprendre. Lorsqu’ils deviennent une habitude, le code devient alors beaucoup plus simple et facile à maintenir. Par contre, il faut comprendre que ces principes ne doivent pas devenir une religion à laquelle on adhère aveuglément sans les remettre en question. Ce sont des principes qu’il faut respecter, mais à pas à n’importe quel prix.
Utilisez la zone de commentaires ci-dessous pour me donner votre opinion! N’hésitez pas à partager et réagir sur les médias sociaux.
Commentaires
8 commentaires
[…] exemple, un coach technique pourrait donner une formation sur les principes SOLID, alors qu’en mode conseil, il pourrait simplement faire un commentaire pendant une revue de code, […]
J’aime bien la manière que c’est synthétisé et expliqué. Parfois c’est un peu lourd quand c’est expliqué sur plusieurs chapitre. Je garde la page dans mes favoris !
Salut Serge!
Merci pour ton commentaire 🙂 N’Hésite pas à t’abonner, nous n’envoyons aucune sollicitation ni publicité.
Bonjour Sylvain,
je trouve ton article très clair et très compact. Merci.
Il me semble aussi avoir repéré quelques coquilles:
Dans la partie ISP:
« public class Vehicle : VehicleWithLights » tu veux dire Vehicle extends ?
Et dans la DIP tu utilises « extends » il me semble qu’il faudrait « implements »
Cordialement,
Julien
Bien vu 🙂 Je remercie ton œil de lynx!
Pour une clarté à la hauteur de cet article, il serait certainement préférable de déclarer l’interface Logger dès le deuxième exemple de DIP.
Autre suggestion : à la place de « on doit inverser les dépendances en implémentant l’injection » écrire « on doit inverser les dépendances en implémentant un mécanisme d’injection ».
Avec mes encouragements
Bien cordialement
Les principes SOLID n’ont pas été inventé par Michael Feathers, mais bel et bien par robert C martin.
Dans ton article tu as confondu les deux, c’est Michael Feathers qui l’a popularisé dans son livre et non pas Robert C martin qui a popularisé Michael Feathers.
Salut! Merci pour ton commentaire 🙂 Par contre, l’acronyme a bel et bien été inventé par Micheal Feathers (2004) basé sur des concepts orientés-objet existants d’Uncle Bob (2000).
La théorie des principes SOLID a été introduite par Martin dans son article Design Principles and Design Patterns de 2000, bien que l’acronyme SOLID ait été introduit plus tard par Michael Feathers.
https://fr.wikipedia.org/wiki/SOLID_(informatique)#cite_note-1 (regarde la source)
Laisser un commentaire