Les principes SOLID ont été conceptualisés pour la première fois par Robert C. Martin en 2000 : Design Principles and Design Patterns.
Ces concepts ont ensuite été développés par Michael Feathers qui a présenté l’acronyme SOLID.
Au cours des 20 dernières années, ces 5 principes ont révolutionné le monde de la programmation orientée objet et changé la façon dont les programmes sont écrits.

Alors, qu’est-ce que SOLID et comment cela aide-t-il à écrire un meilleur code ? En termes simples, les principes de conception de Martin et Feathers encouragent à créer des logiciels plus faciles à maintenir, plus compréhensibles et plus flexibles. Par conséquent, à mesure que la taille des applications augmente, nous pouvons réduire leur complexité et nous épargner bien des problèmes par la suite.
Les 5 concepts suivants constituent les principes SOLID :
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
1 – Single Responsibility (Responsabilité unique)
Commençons par le principe de la responsabilité unique. Comme on peut s’y attendre, ce principe stipule qu’une classe ne doit avoir qu’une seule responsabilité. De plus, elle ne doit avoir qu’une seule raison de changer.
Comment ce principe aide-t-il à créer de meilleurs programmes ? Quelques avantages :
- Tests – Une classe avec une seule responsabilité aura beaucoup moins de cas de tests
- Faible couplage – Moins de fonctionnalités dans une seule classe, moins de dépendances
- Organisation – Les classes plus petites et bien organisées sont plus faciles à rechercher que les classes monolithiques
Prenons, par exemple, une classe pour représenter un simple livre.
Dans ce code, nous stockons le nom, l’auteur et le texte associés à une instance “Book” :
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private String author;
private String name;
private String text;
// Méthodes qui se rapporte directement aux propriétés du livre
public String replaceWordInText (String word) {
return text.replaceAll(word, text);
}
public boolean isWordInText (String word) {
return text.contains(word);
}
}
La classe Book fonctionne, et nous pouvons stocker autant de livres que nous le souhaitons dans notre application.
Mais à quoi bon stocker l’information si nous ne pouvons pas sortir le texte sur notre console et le lire ?
Prudence, et ajoutons une méthode de sortie :
void TextToConsole() {
// Code pour formater et afficher le texte en console
}
Ce code viole toutefois le principe de responsabilité unique.
Pour remédier à ça, nous devons implémenter une classe séparée qui ne s’occupe que de l’impression des textes :
public class BookPrinter {
void TextToConsole (String text) {
// Code pour formater et afficher le texte en console
}
void TextToOtherSupport (String text) {
// Code pour formater et afficher le texte sur un autre support
}
}
Bien, non seulement nous avons développé une classe qui soulage la classe “Book” de ses tâches d’affichage, mais nous pouvons également tirer parti de notre classe BookPrinter pour envoyer notre texte sur d’autres supports.
Qu’il s’agisse de la messagerie, de la journalisation ou de toute autre chose, il faut une classe distincte consacrée à cette seule tâche.
2 – Open / Closed (Ouvert à l’extension / Fermé à la modification)
Passons maintenant au “O”, plus connu sous le nom de principe “open/close“.
En termes simples, les classes doivent être ouvertes à l’extension, mais fermées à la modification. Avec ce principe, nous nous empêchons de modifier le code existant et de provoquer de potentiels bugs dans une application, qui ne s’en portera que mieux.
Bien sûr, la seule exception à la règle est la correction de bugs dans un code existant.
Explorons plus en détail ce concept à l’aide d’un exemple de code. Imaginons que nous ayons implémenté une classe “Guitar” :
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Guitar {
private String model;
private String make;
private int volume;
}
L’application fonctionne, et tout se passe bien. Cependant, au bout de quelques mois, nous décidons que la guitare aurait besoin d’un nouvel attribut.
Il pourrait être tentant d’ouvrir la classe “Guitar” et d’ajouter cet attribut, mais qui sait quelles erreurs cet ajout pourrait provoquer dans notre application.
Au lieu de ça, nous nous en tenons au principe de l’open/closed et nous étendons (extends) simplement notre classe “Guitar” :
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GuitarWithFlowers extends Guitar {
private String flowers;
}
En “étendant” la classe Guitar, nous pouvons être sûrs que notre application existante ne sera pas affectée et nous sommes à l’abri de tous bugs qui pourraient survenir.
3 – Liskov Substitution (Substitution de Liskov)
Le principe de substitution de Liskov, qui est sans doute la contrainte la plus complexe, est le suivant.
En termes simples, si la classe A est un sous-type de la classe B, alors nous devrions pouvoir remplacer B par A sans perturber le comportement de notre programme.
Passons directement au code pour nous aider à comprendre ce concept :
public interface Car {
void turnOnEngine();
void accelerate();
}
Magnifique interface que nous pouvons observer.
Donc, nous définissons une interface “Car” avec quelques méthodes que toutes les voitures devraient être en mesure d’exécuter, allumer le moteur et procéder à une accélération dans les règles de la sécurité routière bien sûr.
Implémentons notre interface et fournissons du code pour les méthodes :
public class MotorCar implements Car {
private Engine engine;
public void turnOnEngine() {
// Allumage du moteur
engine.on();
}
public void accelerate() {
// Accélaration
engine.power(1000);
}
}
Comme le décrit ce morceau de code, nous avons un moteur que nous pouvons allumer, et nous pouvons augmenter la puissance pour l’accélération.
Mais nous sommes en 2021, et Elon Musk est en train d’inonder le marché de l’automobile avec ses voitures électriques :
public class ElectricCar implements Car {
public void turnOnEngine() {
throw new AssertionError("Voiture n'a pas de moteur thermique")
}
public void accelerate() {
// Ca accélère plus fort que du thermique
}
}
En introduisant une voiture sans moteur thermique, nous changeons le comportement de notre programme. C’est une violation de la substitution de Liskov et c’est un peu plus difficile à corriger que les deux principes précédents.
Une solution possible serait de retravailler notre modèle en interface qui prendrait en compte l’état de notre voiture électrique sans moteur thermique, imagé par throw.
4 – Interface Segregation (Ségrégation d’interface)
Le “I” de SOLID signifie “interface segregation” (séparation des interfaces), ce qui signifie simplement que les interfaces les plus grandes doivent être divisées en plus petites.
Nous pouvons donc garantir que les classes d’implémentation ne doivent se préoccuper que des méthodes qui les intéressent.
Commençons par une interface qui décrit le métier de gardien de moutons en haute montagne :
public interface SheepKeeper {
void feedSheep();
void washSheep();
void shearSheep();
}
En tant que gardien de moutons, nous devons tondre et laver ces merveilleux moutons.
Cependant, nous ne sommes que trop conscients des dangers qu’il y a à les nourrir, une main pourrait malencontreusement être mangée. Malheureusement, notre interface est plutôt grande, et nous n’avons pas d’autre choix que d’implémenter le code pour nourrir les moutons.
Pour remédier à ça, nous allons diviser notre interface en 3 interfaces distinctes :
public interface FeedSheep {
void feedTheSheep();
}
public interface WashSheep {
void weshTheSheep();
}
public interface ShearSheep {
void shearTheSheep();
}
Maintenant, grâce à la ségrégation des interfaces, nous sommes libres d’implémenter uniquement les méthodes qui nous intéressent :
public class SheepCleaning implements WashSheep, ShearSheep {
public void washTheSheep() {
// Bien penser à les laver à l'eau douce, la peau est fragile
}
public void shearTheSheep() {
// Tondre soigneusement les poils lisses et soyeux
}
}
Et enfin, nous pouvons affecter la tâche la plus dangereuse à des gardiens de moutons seniors :
public class SeniorFeedSheep implements FeedSheep {
public void feedTheSheep() {
// A réaliser uniquement par des professionnels
}
}
5 – Dependency Inversion (Inversion de dépendance)
Le principe de l’inversion des dépendances fait référence au “découplage” des modules des programmes. Ainsi, au lieu que les modules de haut niveau dépendent des modules de bas niveau, les deux dépendront d’abstractions.
Prenons exemple sur un bout de code et Windows 11 :
public class Windows11Computer {
}
Mais à quoi sert un ordinateur sous Windows 11 sans écran et sans clavier ? Ajoutons un de ces éléments à notre constructor afin que chaque Windows11Computer que nous instancions soit équipé d’un écran 90″ et d’un clavier :
public class Windows11Computer {
private final StandardKeyboard keyboard;
private final Monitor monitor;
public Windows11Computer() {
keyboard = new Keyboard();
monitor = new Monitor();
}
}
Ce code fonctionnera, et nous pourrons utiliser librement le clavier et l’écran dans notre classe Windows11Computer.
Le problème est résolu ? Pas tout à fait. En déclarant le “StandardKeyboard” et le “Monitor” avec le mot-clé new, nous avons étroitement couplé ces 3 classes ensemble.
Non seulement cela rend ‘Windows11Computer’ difficile à tester, mais nous avons également perdu la possibilité de remplacer notre classe ‘Keyboard’ par une autre en cas de besoin. Et nous sommes également coincés avec notre classe Monitor.
“Découplons” notre machine du Keyboard en ajoutant une interface plus générale et en l’utilisant dans notre classe :
public interface Keyboard {
}
public class Windows11Computer {
private final Keyboard keyboard;
private final Monitor monitor;
public Windows11Computer(Keyboard keyboard, Monitor monitor) {
this.keyboard = keyboard;
this.monitor = monitor;
}
}
Ici, nous utilisons le principe d’injection de dépendance pour faciliter l’ajout de la dépendance Keyboard dans la classe Windows11Computer.
Modifions également notre classe “StandardKeyboard” pour qu’elle implémente l’interface Keyboard afin qu’elle puisse être injectée dans la classe “Windows11Computer” :
public class StandardKeyboard implements Keyboard {}
Nos classes sont maintenant “découplées” et communiquent via l’abstraction Keyboard. Si nous le souhaitons, nous pouvons facilement changer le type de keyboard de notre machine avec une implémentation différente de l’interface. Nous pouvons suivre le même principe pour la classe Monitor.