Le principe de substitution de Liskov, nommé d’après Barbara Liskov, stipule que les objets d’une superclasse doivent pouvoir être remplacés par des objets de ses sous-classes sans altérer la correction du programme. Autrement dit, une classe dérivée doit pouvoir être utilisée en lieu et place de sa classe de base sans que cela n’introduise de bugs ou ne change le fonctionnement du programme.
Pourquoi le LSP est-il souvent mal compris ?
Le LSP est parfois mal compris ou mal appliqué en raison de la confusion entre les concepts de « substitution » et d’« extension ». Beaucoup pensent que l’héritage permet simplement d’ajouter des fonctionnalités à une classe de base, mais le LSP impose des contraintes plus strictes : une classe dérivée ne doit pas seulement étendre la classe de base, elle doit se comporter comme la classe de base dans le contexte où celle-ci est attendue. Si ce n’est pas le cas, le contrat de la classe de base est rompu, et donc un non respect du LSP.
Exemple de LSP : Véhicules hybride et thermique
Prenons un exemple simple avec des véhicules. Supposons que nous ayons les classes suivantes :
Vehicule
(classe parente)VoitureThermique
qui représente un véhicule fonctionnant à l’essenceVoitureHybride
qui représente un véhicule hybride, donc fonctionnant avec de l’essence et de l’électricité
Exemple de conception respectant le LSP
Imaginons d’abord une conception qui respecte le LSP :
class Vehicule:
def demarrer(self):
pass
def accelerer(self):
pass
class VoitureThermique(Vehicule):
def demarrer(self):
print("La voiture thermique démarre en utilisant de l'essence.")
def accelerer(self):
print("La voiture thermique accélère en utilisant son moteur à essence.")
class VoitureHybride(Vehicule):
def demarrer(self):
print("La voiture hybride démarre en mode électrique.")
def accelerer(self):
print("La voiture hybride accélère en utilisant le moteur électrique ou le moteur à essence.")
Ici, VoitureThermique
et VoitureHybride
héritent de Vehicule
et implémentent les méthodes demarrer
et accelerer
. Chaque classe dérivée respecte le contrat de la classe de base : elles peuvent être utilisées là où un Vehicule
est attendu sans altérer le fonctionnement du programme.
def tester_vehicule(v: Vehicule):
v.demarrer()
v.accelerer()
# Utilisation avec une VoitureThermique
vw = VoitureThermique()
tester_vehicule(vw) # Fonctionne correctement
# Utilisation avec une VoitureHybride
toyota = VoitureHybride()
tester_vehicule(toyota) # Fonctionne correctement
Dans cet exemple, les deux types de véhicules respectent le LSP. La fonction tester_vehicule
fonctionne correctement, que l’on passe un objet VoitureThermique
ou VoitureHybride
.
Exemple de violation du LSP
Voyons maintenant un exemple qui viole le LSP. Supposons que l’on veuille ajouter une classe Bus
qui hérite également de Vehicule
, mais avec une méthode demarrer
qui impose une vérification spécifique, par exemple pour s’assurer qu’il y a assez de passagers avant de démarrer.
class Bus(Vehicule):
def __init__(self, passagers):
self.passagers = passagers
def demarrer(self):
if self.passagers < 10:
print("Le bus ne peut pas démarrer, pas assez de passagers.")
else:
print("Le bus démarre avec ses passagers.")
def accelerer(self):
print("Le bus accélère.")
Cette implémentation introduit une condition spécifique dans la méthode demarrer
, qui n’existe pas dans la classe Vehicule
. Si l’on remplace un objet de type Vehicule
par un Bus
, cela peut entraîner des comportements inattendus :
def tester_vehicule(v: Vehicule):
v.demarrer()
v.accelerer()
# Utilisation avec un Bus
bus = Bus(5)
tester_vehicule(bus) # Ne fonctionne pas comme prévu, car le bus ne démarre pas si les passagers sont moins de 10
Ici, la classe Bus
viole le LSP car elle introduit une restriction qui n’existe pas dans la classe de base Vehicule
. Le comportement du programme change selon le type d’objet, ce qui n’était pas le cas avec VoitureThermique
et VoitureHybride
.
Avantages de cette approche
- Cohérence : Les sous-types (VoitureThermique, VoitureHybride) peuvent être utilisés partout où le type de base (Vehicule) est attendu, sans surprises.
- Flexibilité : On peut ajouter de nouveaux types de voiture sans modifier le code existant de Vehicule.
- Robustesse : Le code client qui utilise ces classes est moins susceptible de contenir des bugs liés à des comportements inattendus.
En respectant le LSP, nous avons créé une hiérarchie de classes plus logique et plus fiable. Chaque forme conserve son comportement spécifique tout en adhérant à une interface commune, ce qui permet une substitution sans heurts dans le code client.