Le principe de substitution de Liskov (Liskov Substitution Principle – LSP) est le troisième des cinq principes SOLID en programmation orientée objet. Formulé par Barbara Liskov en 1987, il stipule que :
“Si S est un sous-type de T, alors les objets de type T doivent pouvoir être remplacés par des objets de type S sans altérer les propriétés du programme.”
En d’autres termes, une sous-classe doit pouvoir être utilisée en lieu et place de sa classe mère sans provoquer de comportement inattendu.
Une façon simple d’expliquer ce principe est d’imaginer des jouets de construction. Si une pièce est censée remplacer une autre mais ne s’adapte pas correctement, la structure entière devient instable. En programmation, c’est la même chose : si une sous-classe ne respecte pas les attentes de la classe mère, le code devient imprévisible et peut causer des erreurs difficiles à corriger.
Ce principe est souvent illustré par l’exemple du carré et du rectangle, une relation d’héritage qui, bien que valide en mathématiques, ne l’est pas nécessairement en programmation orientée objet.
Pourquoi est-ce important ?
Le non-respect du LSP peut entraîner :
- Une violation du polymorphisme, rendant le code moins flexible. Imaginez une classe
Oiseau
avec une méthodevoler()
. Si on crée une sous-classePingouin
qui hérite deOiseau
mais lève une exception dansvoler()
, alors un code qui manipule unOiseau
ne pourra pas toujours voler comme prévu, ce qui casse le polymorphisme. - Des bugs difficiles à identifier, car les remplacements d’instances ne respectent pas les attentes. Une classe
Voiture
dispose d’une méthoderouler()
. Si une sous-classeBateauVoiture
hérite deVoiture
mais modifierouler()
pour naviguer au lieu de rouler, un programme s’attendant à un comportement terrestre pourrait produire des erreurs imprévisibles - Une maintenance plus compliquée, car le code devient plus fragile et imprévisible. Une classe
Animal
avecfaireDuBruit()
est héritée parChien
etPoisson
. SiPoisson
ne peut pas faire de bruit et lève une exception, les développeurs devront sans cesse adapter le code pour gérer des cas particuliers, rendant la maintenance plus complexe.
Mauvais exemple : Violation du LSP avec Rectangle et Carré
Prenons l’exemple populaire du carré et du rectangle, cité plus haut :
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Forcer un carré
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // Forcer un carré
}
}
Pourquoi est-ce une violation du LSP ?
Le problème ici est que Square
ne respecte pas les attentes de Rectangle
. Si une méthode manipule un Rectangle
, elle s’attend à pouvoir modifier indépendamment sa largeur et sa hauteur, ce qui n’est pas le cas pour un Square
.
Testons ce comportement :
public static void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(10);
System.out.println("Aire attendue: 50, Aire calculée: " + rect.getArea());
}
public static void main(String[] args) {
Rectangle rect = new Rectangle();
testRectangle(rect);
Rectangle square = new Square();
testRectangle(square); // Problème ici !
}
Sortie attendue :
Aire attendue: 50, Aire calculée: 50
Aire attendue: 50, Aire calculée: 100 <-- Erreur due à la violation du LSP
Ici, le comportement change en fonction de l’implémentation, ce qui brise l’interchangeabilité des classes.
Bon exemple : Respect du LSP avec une conception améliorée
Plutôt que d’avoir une relation d’héritage inappropriée, on peut utiliser la composition et une interface commune.
Refactorisation correcte :
interface Shape {
int getArea();
}
class Rectangle implements Shape {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
public int getArea() {
return side * side;
}
}
Testons maintenant :
public static void testShape(Shape shape) {
System.out.println("Aire calculée: " + shape.getArea());
}
public static void main(String[] args) {
Shape rect = new Rectangle(5, 10);
testShape(rect);
Shape square = new Square(5);
testShape(square);
}
Sortie correcte :
Aire calculée: 50
Aire calculée: 25
Cette refactorisation respecte le LSP, car les objets Rectangle
et Square
respectent leur propre logique métier tout en partageant une interface commune Shape
.
Autre mauvais exemple : Violation du LSP avec Oiseaux
Un autre exemple courant est l’utilisation incorrecte d’une hiérarchie d’héritage entre les oiseaux :
class Oiseau {
public void voler() {
System.out.println("Cet oiseau vole");
}
}
class Pingouin extends Oiseau {
@Override
public void voler() {
throw new UnsupportedOperationException("Un pingouin ne peut pas voler");
}
}
Pourquoi est-ce une violation du LSP ?
Un code s’attendant à manipuler un Oiseau
supposera que l’oiseau peut voler. Cependant, en remplaçant un Oiseau
par un Pingouin
, nous obtenons une exception non prévue qui casse le comportement attendu.
Solution correcte :
Une meilleure solution consiste à utiliser des interfaces :
interface Oiseau {}
interface OiseauVolant extends Oiseau {
void voler();
}
class Moineau implements OiseauVolant {
public void voler() {
System.out.println("Le moineau vole");
}
}
class Pingouin implements Oiseau {
// Aucun comportement de vol ici
}
Ainsi, les classes ne forcent pas un comportement non applicable et restent substituables sans créer d’erreur.
Conclusion
Le principe de substitution de Liskov est essentiel pour écrire un code robuste et évolutif. Pour l’appliquer correctement :
- Évitez de modifier des comportements attendus dans une sous-classe.
- Ne forcez pas un héritage s’il n’a pas de sens.
- Utilisez des interfaces ou la composition lorsque les comportements diffèrent trop.
En respectant ce principe, vous garantissez un code plus flexible, réutilisable et facile à maintenir.
Ressources complémentaires
Qu’en pensez-vous ? Avez-vous rencontré des violations du LSP dans vos projets ? Partagez vos expériences en commentaire !