Principes S.O.L.I.D - Liskov substition principle (LSP)

1. Le principe de substitution de Liskov

Barbara Liskov donne la définition suivante du sous-typage :

"What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T."
Barbara Liskov, Data Abstraction and Hierarchy, SIGPLAN Notices, 23,5 (1988).

En clair, pour prétendre à l'héritage, une sous-classe doit être conçue de sorte que ses instances puissent se substituer à des instances de la classe de base partout où cette classe de base est utilisée.

2. L'héritage comme offre de service

Cette définition de l'héritage place l'accent sur la classe de base, qui se présente comme une véritable offre de service affichée par chaque sous-classe. En langage objet, cela revient à dire que la classe de base est une interface exportée par toutes ses sous-classes.

Ce concept rejoint celui du "Design by contract" de Bertrand Meyer, l'interface représentant un véritable contrat passé entre chaque sous-classe et les classes susceptibles de l'utiliser.

En insistant sur cette approche de l'héritage, le principe de substitution s'oppose à une pratique répandue dans laquelle l'héritage est mis en œuvre pour factoriser du code entre plusieurs classes.

3. La substitution parfaite ?

Selon ce principe, l'héritage est donc d'autant plus efficace que la substitution est parfaite. Comme toujours cela est plus facile à dire qu'à faire : tôt ou tard apparaît une sous-classe qui ne "rentre pas dans l'interface", et là deux solutions s'offrent au développeur :
  • Soit il reste fermé sur l'interface et impose aux utilisateurs de la sous-classe de recourir au "downcast" pour traiter le cas particulier, ce qui entraîne une violation de l'OCP,
  • Soit il élargit l'interface pour couvrir ce cas particulier, mais il impose alors aux autres sous-classes une partie d'interface qui ne leur correspond pas (typiquement sous la forme de méthodes définies dans l'interface mais dont l'implémentation restera vide dans les classes dérivées), avec cette fois-ci une violation du LSP.
Ce problème n'a pas de solution simple, mais la première solution nous semble préférable dans la mesure où l'intégrité du "contrat" est respectée. En d'autres termes, dans les cas où l'interface ne convient pas on préfèrera recourir au downcast et laisser les problèmes où ils apparaissent plutôt que de tenter de masquer le problème en corrompant l'interface.