Arrêtez de développer n'importe comment ! - Le principe de substitution de Liskov

En programmation informatique, SOLID est un acronyme représentant cinq
principes de base pour la programmation orientée objet. Ces cinq principes sont censés apporter une ligne directrice permettant le développement de logiciels plus fiables, plus robustes, plus maintenables, plus extensibles et plus testables.

Aujourd'hui nous nous intéressons au «L» de SOLID :

Liskov Substitution Principle

  • Liskov Substitution Principle ou « principe de substitution de Liskov » qui définit qu'une instance de type T doit pouvoir être remplacée par une instance de type G, tel que G sous-type de T, sans que cela modifie la cohérence du programme. En d'autres termes, il s'agit de conserver les mêmes prototypes ainsi que les mêmes conditions d'entrée / sortie lorsqu'une classe dérive une classe parente et redéfinit ses méthodes. Cela vaut aussi pour les types d'exceptions. Si une méthode de la classe parente lève une exception de type E alors cette même méthode redéfinie dans une sous-classe C' qui hérite de C doit aussi lever une exception de type E dans les mêmes conditions.

Derrière cette définition un peu barbare nous pouvons prendre un exemple simple : Un rectangle.

Prenons une classe Rectangle représentant les propriétés d'un rectangle : hauteur (height), largeur (width). On lui associe donc des accesseurs pour accéder et modifier la hauteur et la largeur librement :

class Rectangle(var width: Int, var height: Int) {}

Imaginons maintenant une classe « carré » (Square). En mathématiques, un carré est un rectangle. Il est donc logique de faire hériter notre classe Square de la classe Rectangle.

On définit comme postcondition la règle : les « quatre côtés du carré doivent être égaux ». C’est-à-dire que le constructeur de Square n'a en théorie besoin qu'un seul paramètre.

📝 Rendez la classe Rectangle ouverte à l'extension (open)

👉 Question 1

  • À quoi pourrait ressembler la classe Square ? Complétez le code suivant en remplaçant les ?? par les bonnes instructions :
class ??(var side: Int): Rectangle(??, side) {
    ?? fun getArea() : ?? {
        return ?? * side
    }
}

Avec cette implémentation, on s'attend à pouvoir utiliser une instance de type Square n'importe où un type Rectangle est attendu.

Problème : Un carré ayant par définition quatre côtés égaux, il convient de restreindre la modification de la hauteur et de la largeur pour qu'elles soient toujours égales. Néanmoins, si un carré est utilisé là où, comportementalement, on s'attend à interagir avec un rectangle, des comportements incohérents peuvent subvenir : les côtés d'un carré ne peuvent être changés indépendamment, contrairement à ceux d'un rectangle. Une mauvaise solution consisterait à modifier les setter du carré pour préserver l'invariance de ce dernier. Mais ceci violerait la postcondition des setter du rectangle qui spécifie que l'on puisse modifier hauteur et largeur indépendamment.

Imaginons, une fonction utilitaire qui nous permettrais de doubler la taille de n'importe quel type rectangle :

fun doubleTheSizeOf(rectangle: Rectangle) {
    rectangle.width = rectangle.width * 2
    rectangle.height = rectangle.height * 2
}

📝 Écrivez le code qui permet de doubler la taille du rectangle

👉 Question 2

  • Que constatez-vous ?

Par la suite faites de même avec une instance de classe carrée :

val square = Square(3)
println(square.getArea())
doubleTheSizeOf(square)
println(square.getArea())

👉 Question 3

  • Que constatez-vous ?
  • À quoi cela est-il dû ?
💡 Une solution pour éviter ces incohérences est de retirer la nature Mutable des classes Carré et Rectangle. Autrement dit, elles ne sont accessibles qu'en lecture. Il n'y a aucune violation du LSP, néanmoins on devra implémenter des méthodes "hauteur" et "largeur" à un carré, ce qui, sémantiquement, est un non-sens.

📝 Réécrivez le code qui permettra d'exécuter les lignes de code précédentes de façon correcte. C’est-à-dire qu'à partir de la même fonction utilitaire, le calcul de l'air du carré et du rectangle soit correct.