• 8 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 31/05/2022

« L » pour le principe de substitution de Liskov

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Qu’est-ce que la substitution de Liskov ?

L’héritage commence par une bonne idée. Vous avez un concept existant, et vous voulez en ajouter un autre. Ce nouveau concept consiste simplement en une implémentation plus spécifique du concept original. Facile : dérivez une nouvelle classe !

Mais avec le temps, il est facile d’abuser de l’héritage. Lorsqu’il y a trop de couches d’héritage, c’est normal de perdre le fil de ce que toutes les classes spécifiques sont censées faire. À la fin, une classe incapable de faire quelque chose que ses classes parentes doivent pouvoir faire est ajoutée, et le système est cassé.

Il s’agit d’une violation de la substitution de Liskov, qui dit que :

Les objets des classes enfants doivent pouvoir remplacer les objets des classes parentes sans nuire à l’intégrité de l’application.

Autrement dit :

Tout code appelant des méthodes sur des objets d’un type spécifique doit continuer à fonctionner lorsque ces objets sont remplacés par des instances d’un sous-type.

Qu’est-ce qu’un sous-type ?

Ce principe porte le nom de Barbara Liskov, l’une des premières femmes à avoir obtenu un doctorat en sciences informatiques aux États-Unis. Elle est également la créatrice des langages de programmation Argus et CLU.

Elle a présenté pour la première fois le principe de substitution de Liskov en 1987, lors de son discours d’ouverture de conférence intitulé « Data Abstraction » (« Abstraction des données »).

Barbara Liskov

En tant qu’informaticienne pure et dure, elle l’a défini de façon formelle ainsi :

Si Φ(x) est une propriété démontrable pour tout objet x de type T, alors, Φ(y) doit être vrai pour les objets y de type S, lorsque S est un sous-type de T.

Mais cette définition semble très théorique, donc si vous êtes comme moi, il vous sera probablement plus facile de penser à ce principe en utilisant les définitions précédentes !

Pourquoi utiliser le principe de substitution de Liskov ?

Disons que vous ayez une classe nommée félin. Elle possède une méthode nommée  manger()  . Vous pouvez passer une marque de nourriture pour chats standard – par exemple « Chaton Gourmet » – dans la méthode manger, et le chat (qui est un félin) ira très bien. 🐈 Si vous ajoutez une nouvelle classe, tigre, qui est aussi une sorte de félin, la méthode manger sera implémentée à nouveau. 🐅

En revanche, les tigres ne mangent pas de nourriture déshydratée en sachet. Ils mangent de la viande crue. Ce n’est pas le même comportement que celui que vous avez vu précédemment. Surprise ! 😬

Le problème survient lorsque vous pensez que le fait de passer la nourriture “Chaton Gourmet” dans la méthode  manger  fonctionne pour tous les félins. Un tigre est un félin, donc ça devrait marcher. Mais ça ne marche pas. Par conséquent, vous ne pouvez pas simplement mettre un tigre dans votre système où vous aviez un félin.

Cet exemple viole le principe de substitution de Liskov : vous ne pouvez pas remplacer la classe de base (félin) avec une classe dérivée (tigre) sans affecter le reste du système.

Le principe de substitution de Liskov vous aide en limitant votre utilisation de l’héritage. Bien qu’il soit facile de créer des classes de niveau bas et d’implémentation concrète (après tout, elles font ce que vous voulez qu’elles fassent), il est préférable de prendre un peu de recul et de réfléchir à une abstraction de haut niveau.

Par exemple, essayons de résoudre le problème du félin et du tigre. Vous avez là en réalité deux classes concrètes (c’est-à-dire, d’implémentation spécifique). Il vous faut introduire quelques interfaces/abstractions de plus haut niveau :

  • Carnivore  , qui ne mange que de la viande.

  • Omnivore  , qui mange de la viande et d’autres choses (comme du Chaton Gourmet).

Maintenant, quand vous ajoutez n’importe quel animal, vous pouvez lui faire implémenter l’une de ces deux interfaces.

Il semblait logique au départ de considérer le tigre comme un type de félin. Mais vous avez vu le problème qui est survenu au stade de l’implémentation. Il est souvent difficile de reconnaître à l’avance quand le principe de Liskov sera enfreint. Cependant, lorsque vous découvrez une violation, vous devez repenser votre utilisation de l’héritage. Par conséquent, lorsque vous voulez utiliser l’héritage, posez-vous les questions suivantes :

  • Est-ce que la classe dérivée a une implémentation significative pour toutes les méthodes surchargées ? Si oui, c’est une bonne chose. ✅

  • Est-ce que l’implémentation d’une méthode surchargée sortirait de l’ordinaire et risquerait d’avoir pour conséquence de lancer une exception ? Si oui, c’est mauvais signe. ❌

  • Est-ce que l’implémentation d’une méthode surchargée ignorerait l’appel, et ne ferait rien ? Habituellement c’est mauvais signe, mais ça pourrait se justifier.

    Observez si la classe dérivée agit ainsi pour :

    ○      Une seule méthode. ✅

    ○      De nombreuses méthodes. ❌

Comment appliquer la substitution de Liskov à notre code ?

Dans notre jeu de cartes, la vue est responsable de la collecte des réponses nécessaires de la part de l’utilisateur, comme les noms des joueurs, ainsi que de la présentation de la progression du jeu.

Imaginez que nous souhaitions diffuser notre palpitante partie de cartes à un public mondial. Pour ce faire, nous pourrions créer un sous-type de notre  View  , nommé par exemple  BroadcastView  (VuePourDiffusion), qui accomplirait d’autres choses pour la diffusion, comme l’affichage de l’heure locale, et l’ajout de commentaires. Comme nous utilisons le MVC, il devrait être facile de remplacer simplement notre objet  View  par un objet  BroadcastView  .

Parcourez la classe  View  actuelle et voyez si vous arrivez à trouver le problème relatif à Liskov qu’elle va poser :

class View:
def prompt_for_new_player(self):
# code
def show_player_and_hand(self, player_name, hand):
# code
def prompt_for_flip_cards(self):
# code
def show_winner(self, winner_name):
# code
def prompt_for_new_game(self):
# code

engrenages

Le problème, c’est que  BroadcastView  ne doit pas accepter d’instructions du public mondial sur les noms des joueurs, le moment où retourner les cartes, ou si l’on rejoue ou non. Seuls les joueurs devraient avoir ces options !

BroadcastView  n’obéit pas à la substitution de Liskov, car cette classe ne peut pas accomplir certaines des choses que sa classe parente –  View  – implémente. La hiérarchie de classe n’est pas bonne.

Alors, que pourrions-nous faire d'autre ?

Dès que nous voulons créer un composant de vue pour la diffusion, nous aurons besoin d’un autre composant qui interagisse avec les joueurs. Nous pourrions donc créer une classe  PlayersAndBroadcastView  (JoueursEtVuePourDiffusion) qui gère tout.

Mais ceci enfreint l’un des autres principes SOLID – vous voyez lequel ?

engrenagesC’est bien ça – il s’agit du principe de responsabilité unique ! `PlayersAndBroadcastView` se présente au public et aux joueurs. Il vaudrait mieux séparer ces deux éléments en deux classes distinctes.

Dans les deux prochains chapitres, une fois que nous aurons vu le reste des principes SOLID, nous implémenterons une excellente solution pour gérer les collectes des vues, tout en respectant tous ces principes.

En résumé

  • Le principe de substitution de Liskov s’applique aux hiérarchies d’héritage. Il est enfreint lorsqu’une classe dérivée ne peut pas prendre la place d’une classe de base sans casser le système.

  • Pour vous assurer de ne pas enfreindre cette règle, essayez de penser d’abord aux abstractions/interfaces de haut niveau, avant d’envisager les implémentations de bas niveau/concrètes.

Au chapitre suivant, nous nous intéresserons à l’utilité de faire en sorte que les interfaces restent petites, avec le principe de ségrégation des interfaces.

Exemple de certificat de réussite
Exemple de certificat de réussite