SOLID Partie III : Liskov Substitution Principle

Introduction

Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T.

 Barbara Liskov, Jeannette Wing, “A behavioral notion of subtyping”,1994

Liskov et Wing définissent un sous-type (ou une sous-classe) par substituabilité : S est un sous-type de T si l’on peut toujours remplacer un objet de type T par un objet de type S.
Nous n’allons pas rentrer ici dans les aspects théoriques de cette définition, ni détailler les travaux de Barbara Liskov, mais retenons simplement ceci : une sous-classe n’est une sous-classe que si elle se comporte comme sa classe mère, c’est-à-dire que l’on peut remplacer une instance de la classe mère par une instance de la classe fille.

Le corollaire de ce principe est qu’une sous-classe ne doit pas rompre un “contrat” établi par la classe mère. C’est le Design By Contract de Bertrand Meyer

Principes de Design by Contract

Ces principes assurent la substituabilité. Enfreindre ces principes oblige à connaître la hiérarchie des classes et les détails de l’implémentation de la classe fille utilisée, rompant du même coup l’Open/Closed Principle

  • Une classe fille ne doit pas avoir de préconditions plus fortes que la classe mère.
  • Une classe fille ne doit pas avoir de postconditions plus faibles que la classe mère
  • Une classe fille ne doit pas lever d’exceptions qui ne soient levées par la classe mère (ou qui ne soient des exceptions dérivées, obéissant au mêmes principes).

Exemples de violations du LSP

Deux exemples volontairement simples de violation du LSP.

Exemple #1 : exceptions différentes dans la classe fille

class Rectangle {
  /*...*/
  public:
    void setDimensions(double largeur, double longeur) {
      this->largeur = largeur;
      this->longueur = longueur;
    }
}

class Carre : public Rectangle {
  /*...*/
  public:
    void setDimensions(double largeur, double longueur) {
      if (largeur != longueur) {
        throw 1;
      }
      this->largeur = this->longueur = longueur;
    }
}
Exemple #2 : postconditions plus faibles dans la classe fille
class Rectangle {
  /*...*/
  public:
    void setLargeur(double largeur) {
      this->largeur = largeur;
    }
    void setLongueur(double longueur) {
      this->longueur = longueur;
    }
  /*...*/
}

class Carre : public Rectangle {
  /*...*/
  public:
    void setLargeur(double largeur) {
      this->longueur = this->largeur = largeur;
    }
    void setLongueur(double longueur) {
      this->longueur = this->largeur = longueur;
    }
  /*...*/
}

void testAire (Rectangle * r) {
  r->setLargeur(5);
  r->setLongueur(4);
  assert(r->getLargeur() * r->getLongueur() == 20);
}

Ce second exemple est l’exemple employé par Robert C. Martin. Lorsque l’on passe à testAire un objet de classe Carre, le test échoue. Cela peut sembler paradoxal car un carré est un rectangle (sic.), mais un Carre ne peut pas être un Rectangle car il ne se comporte pas pareil, c’est à dire que les suppositions que l’on peut raisonnablement faire sur l’un ne peuvent être faites sur l’autre. Ainsi testAire suppose, à juste titre, que la longueur et la largeur d’un Rectangle évoluent indépendamment, ce qui n’est pas le cas d’un carré. C’est donc dans le comportement attendu que se caractérise la relation d’héritage.

Conclusion

Le Liskov Substitution Principle est à méditer longtemps car il nous force à revoir la définition habituelle que nous faisons de la notion d’héritage. En effet il nous enseigne que c’est dans lecomportement commun, comportement publiquement attendu, qui définit une sous-classe et non dans sa nature intrinsèque.

Pour aller plus loin

One thought on “SOLID Partie III : Liskov Substitution Principle

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.