Les principes S.O.L.I.D. illustrés
Introduction
S’ils n’ont rien à voir avec Solid Snake, héros du mythique jeu Metal Gear Solid, les cinq principes englobés dans l’acronyme S.O.L.I.D ont pourtant eux aussi cette faculté à nous aider à traverser les obstacles. Cette fois-ci, ces derniers sont ceux de la Programmation Orientée Objet (POO).
Un peu d’Histoire
Dévoilés au grand jour par Robert C. Martin (fondateur du Manifeste Agile) dans son livre Design Principles and Design Patterns (2002), les cinq piliers d’aide à la Programmation Orientée Objet sont essentiels au bon fonctionnement des méthodes Agiles.
Au début des années 2000, lorsque Michael Feathers et Robert C. Martin se sont réunis avec 15 autres personnalités de la conception software, les deux hommes ont apporté une réponse précise et fonctionnelle aux problématiques de conception et d’évolutivité endémique de la conception objet.
Leur solution ? 5 principes permettant d’adapter les méthodes de travail et d’apposer un cadre de travail visant à faciliter le travail des développeurs. L’acronyme en lui-même provient en revanche de l’auteur de Working Effectively With Legacy Code (Michael C. Feathers).
S comme Single Responsibility Principle (SRP)
Le principe de Single Responsibility (responsabilité limitée/unique) est aussi simple que son nom l’indique. Prenez une classe et tentez d’en expliquer sa fonction en entrant dans les détails.
Dès lors que cette même classe est amené à gérer plusieurs responsabilités (utilisation du « et », « ou »), elle sort du principe. Certaines petites attentions vous permettront néanmoins de vérifier ça au fur et à mesure de l’écriture de votre code. Dès lors que plusieurs tâches sont effectuées dans la même classe : arrêtez et subdiviser.
Exemple d’une classe (réduite) :

Ici, la classe voiture a plusieurs responsabilités, elle permet de se déplacer et également de changer la fréquence de la radio. Cela ne respecte pas le principe de responsabilité unique. Afin de corriger ce problème, il convient de subdiviser le code en deux classes distinctes.
Ainsi, voici une voiture qui possède un poste de radio :

Le poste de radio pouvant modifier la fréquence de la radio :

O comme Open-Closed
La traduction littérale française « Ouvert / Fermé » suffit à elle-même à comprendre de quoi il s’agit. S’il faut tout de même faire attention aux bugs, il ne faut pas (jamais) avoir de modifications à apporter à une classe.
Le principe de « Open / Closed » a pour but d’énoncer le fait qu’une classe ou une fonction doit être ouverte à l’extension et fermée à la modification. Une fois qu’une classe est clairement définie, qu’elle a été revue et qu’elle a passée tous les tests nécessaires, il ne faut plus y toucher !
Toutes les nouvelles fonctionnalités doivent être une extension car une modification de la classe ou de ses fonction risque d’engendre des régressions.
Afin de réaliser des extensions de classe, plusieurs méthodes sont possibles telles que le polymorphisme (avec les modificateurs Abstract/ Virtual et Override), la création de classes abstraitesen regroupant ce qui est commun et en définissant les fonctions abstraites qui seront à implémenter dans les classes enfants, ou encore la création d’une interface clairement définie que l’on implémente dans les classes nécessitant l’extension.
L comme« Liskov Substitution Principle » (LSP)
Le principe de « Liskov Substitution » ou « Substitution de Liskov » a été nommé ainsi en l’honneur de Barbara Liskov, première informaticienne – et lauréate du prix Turing – à avoir fait part de ce principe au début des années 90 devant la communauté des développeurs de logiciels.
Passé cet hommage, concentrons-nous sur le côté technique du principe. Il existe diverses façons de l’expliquer, vous les retrouverez ci-après. En des termes propres à nos chers développeurs, la substitution de la classe mère par une classe fille doit pouvoir donner un résultat semblable lors de l’activation des fonctionnalités. Si tel n’est pas le cas, le principe n’est pas respecté. Lorsque vous êtes scrupuleusement attentif à ce dernier, vous noterez vite une augmentation de l’encapsulation et une diminution du couplage.
Formule mathématique
La formulation mathématique de Liskov et Wing : « Si q(x) est une propriété démontrable pour tout objet x de type T, alors q(y) est vraie pour tout objet y de type S tel que S est un sous-type de T. »
Reprenons l’exemple de notre classe « voiture » :

Pour appliquer le principe de subsitution de Liskov – car nous avons différents types de véhicules (autonomes, classiques, hybdrides, etc.), nous aurons le code suivant qui affiche une interface définissant les fonctions de notre voiture :

Une classe abstraite pour définir les fonctions et les propriétés communes de notre voiture :

Puis nos deux types de véhicules :

Mais concrètement, à quoi ça sert ? Le but ici est de pouvoir dire que VoitureClassique et VoitureAutonome ont les mêmes propriétés que Voiture et qu’elles implémentent l’interface IVoiture.
Donc, en utilisant l’interface IVoiture, nous pouvons appeler les 3 fonctions : Avancer (), ChangerDirection() et Stationner() sans avoir à se soucier du type de voiture que nous utilisons :

Exemple d’appel :

I comme Interface Segregation Principle
Utile notamment dans la décomposition du code et l’usage des tests unitaires, le principe d’ Interface Segregation Principle, en français « ségrégation/séparation des interfaces », est une technique visant à concentrer la création de multiples interfaces autour d’un seul et même domaine plutôt que de n’en créer qu’une, trop conséquente, qui engloberait des informations non nécessaires.
Sa définition la plus simpliste et populaire est la suivante : « Les clients d’une entité logicielle ne doivent pas avoir à dépendre d’une interface qu’ils n’utilisent pas. »
Imaginons cette fois un constructeur automobile qui vend des voitures avec un toit ouvrant en option. Nous aurons une interface définissant les fonctions de ce véhicule avec la fonctionnalité toit ouvrant :

Le véhicule doit alors implémnter toutes les fonctions de l’interface IVoiture, mais que ce passe-t-il si le véhicule ne possède pas de toit ouvrant ? La fonction GererToitOuvrant() ne sera pas implémenter et cela peut résulter en l’apparition d’exceptions indésirables :

Afin d’éviter ce genre de soucis, et afin de respecter le principe de segrégation des interfaces, nous allons créer deux interfaces clairement définies :

Désormais, si nous avons un véhicule avec l’option de toit ouvrant, il faudra implémenter l’interface IVoiture et également l’interface IToitOuvrant :

Si notre véhicule n’a pas de toit ouvrant :

D comme Dependency Inversion Principle (DIP)
Le principe d’inversion de dépendance est souvent – et à tort – considéré comme un principe secondaire puisqu’il résulte de l’utilisation attentive de l’Open Close Principle et du Liskov Substitution Principle. Sa définition la plus communément admise se divise en deux axes :
- Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d’abstractions.
- Les abstractions ne doivent pas dépendre des détails <==> Les détails doivent dépendre des abstractions.
À l’origine de la Programmation Orientée Objet, la programmation modulaire intime le fait qu’aucun héritage n’est possible puisque le module se contente d’importer et exporter des variables, des fonctions ou des classes.
Ici par « module de haut niveau », il faut comprendre les modules « business » tandis que pour les bas niveaux, basons-nous sur la déclaration (bien qu’à nuancer) d’Alan Perlis : « un langage de programmation est bas niveau lorsqu’il nécessite de faire attention aux choses qui ne sont pas pertinentes ».
- La mise en pratique est très simple et se déroule en quatre étapes clés :
- Analyser et lister les modules de bas niveau
- Placer une abstraction pour chacun des éléments (interface ou classe de type Factory)
- Rédiger les implémentations qui vont respecter ces interfaces
- L’application doit définir quelle implémentation sera nécessaire pour chaque interface