Le Principe de Substitution de Liskov, formulé par Barbara Liskov, est une extension du Principe Ouvert/Fermé. Il garantit que l'héritage est utilisé de manière correcte et prévisible. Sa définition formelle est :
Si S est un sous-type de T, alors on doit pouvoir substituer des objets de type T par des objets de type S sans altérer les propriétés désirables du programme.
En termes plus simples : une sous-classe (ou une classe qui implémente une interface) doit être complètement substituable à sa classe parente (ou son interface) sans que le code qui l'utilise ne s'en rende compte et sans créer de comportement inattendu.
Une sous-classe ne doit pas être "plus stricte" que sa classe parente. Elle doit se comporter de la même manière.
Le cas d'école classique est le problème du "carré qui est un rectangle". Mathématiquement, un carré est un rectangle. Essayons de modéliser cela avec l'héritage.
<?php
class Rectangle
{
protected int $width;
protected int $height;
public function setWidth(int $width): void
{
$this->width = $width;
}
public function setHeight(int $height): void
{
$this->height = $height;
}
public function getArea(): int
{
return $this->width * $this->height;
}
}
// Un carré est un rectangle... n'est-ce pas ?
class Square extends Rectangle
{
// Pour qu'un carré reste un carré,
// changer la largeur doit aussi changer la hauteur.
public function setWidth(int $width): void
{
$this->width = $width;
$this->height = $width; // <-- Effet de bord inattendu
}
public function setHeight(int $height): void
{
$this->width = $height;
$this->height = $height; // <-- Effet de bord inattendu
}
}Maintenant, écrivons un code client qui s'attend à manipuler un Rectangle.
function printArea(Rectangle $rectangle): void
{
$rectangle->setWidth(5);
$rectangle->setHeight(4);
// Le code client s'attend logiquement à ce que l'aire soit 5 * 4 = 20
$expectedArea = 20;
$actualArea = $rectangle->getArea();
echo "Aire attendue : $expectedArea, Aire calculée : $actualArea\n";
}Utilisation
$rect = new Rectangle();
printArea($rect); // Affiche : Aire attendue : 20, Aire calculée : 20 (OK)
$square = new Square();
printArea($square); // Affiche : Aire attendue : 20, Aire calculée : 16 (BUG !)Pourquoi est-ce un problème ?
- Nous avons substitué un
Rectanglepar unSquare(son sous-type), mais cela a changé le comportement du programme. La fonctionprintAreane fonctionne plus comme prévu. - La classe
Squareviole le contrat de la classeRectangle. Un utilisateur deRectangles'attend à ce quesetWidth()ne modifie QUE la largeur. La classeSquarea un comportement surprenant et brise cette attente. - Pour faire fonctionner ce code, il faudrait ajouter un
if ($rectangle instanceof Square)dans la fonctionprintArea, ce qui violerait le Principe Ouvert/Fermé.
La solution est de reconnaître que, du point de vue du comportement, un Square n'est pas un Rectangle substituable. L'héritage n'est pas le bon outil ici. On préférera une abstraction commune.
Une meilleure modélisation serait de se baser sur le comportement, pas sur la taxonomie mathématique.
<?php
interface Shape
{
public function getArea(): int;
}
// Chaque classe implémente l'interface, mais sans lien d'héritage entre elles.
class Rectangle implements Shape
{
private int $width;
private int $height;
public function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
public function getArea(): int
{
return $this->width * $this->height;
}
}
class Square implements Shape
{
private int $side;
public function __construct(int $side)
{
$this->side = $side;
}
public function getArea(): int
{
return $this->side * $this->side;
}
}Quels sont les bénéfices ?
- Prévisibilité : Il n'y a plus de comportement inattendu. Chaque classe a sa propre logique interne, mais elles respectent toutes le contrat de l'interface
Shape. - Pas de substitution incorrecte : Le code client qui a besoin de définir une largeur ET une hauteur dépendra explicitement d'un
Rectangle, pas d'uneShapegénérique. - L'héritage est utilisé à bon escient : Le LSP nous force à réfléchir si l'héritage est la bonne solution. Souvent, la composition ou les interfaces sont de meilleures alternatives.
Pour qu'une sous-classe respecte le LSP, elle doit suivre ces règles :
- Les préconditions ne peuvent pas être renforcées (ses méthodes ne doivent pas exiger plus que la méthode parente).
- Les postconditions ne peuvent pas être affaiblies (ses méthodes doivent garantir au moins autant que la méthode parente).
- Les types des paramètres de la méthode doivent être les mêmes ou plus abstraits (contravariance).
- Le type de retour de la méthode doit être le même ou un sous-type (covariance).
- Elle ne doit pas lever d'exceptions que la classe parente n'est pas censée lever.
| Principe | Définition | Bénéfice Principal |
|---|---|---|
| LSP | Une sous-classe doit être entièrement substituable à sa classe parente sans altérer la cohérence du programme. | Garantit que l'héritage est utilisé de manière fiable et prévisible, évitant des bugs subtils et renforçant la stabilité du code. |