Skip to content

Instantly share code, notes, and snippets.

@DamienDabernat
Last active February 26, 2025 12:31
Show Gist options
  • Save DamienDabernat/b05dd2e2942301851bb095ada4720541 to your computer and use it in GitHub Desktop.
Save DamienDabernat/b05dd2e2942301851bb095ada4720541 to your computer and use it in GitHub Desktop.
Arrêtez de développer n'importe comment ! - Le principe de substitution de Liskov

Arrêtez de développer n'importe comment ! (L du SOLID)

En programmation informatique, SOLID est un acronyme représentant cinq principes fondamentaux pour la programmation orientée objet. Ces principes aident à rendre le code plus fiable, maintenable, extensible et testable.

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

Tous les exercices sont faisables entièrement en ligne depuis site de Kotlin

Liskov Substitution Principle (LSP)

Le principe de substitution de Liskov stipule qu'une instance d'un type T doit pouvoir être remplacée par une instance de son sous-type G, sans altérer la cohérence du programme.

Autrement dit, lorsqu'une classe dérive d'une autre et redéfinit ses méthodes, elle doit conserver :

  • Les mêmes prototypes de méthodes.
  • Les mêmes préconditions et postconditions.
  • Les mêmes types de retour.
  • Les mêmes types d'exceptions (si une méthode lève une exception de type E, alors une redéfinition dans une sous-classe doit lever E ou un sous-type de E).

Exemple : le rectangle et le carré

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

Prenons une classe Rectangle qui possède deux propriétés : la hauteur (height) et la largeur (width). Elle est définie ainsi :

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

Imaginons maintenant une classe Square. En mathématiques, un carré est un rectangle. Il semblerait donc logique de faire hériter Square de Rectangle.

Cependant, un carré a une contrainte supplémentaire : ses quatre côtés doivent être égaux. Cela signifie que nous devrions imposer que width == height dans la classe Square.


📝 Étape 1 : Rendre la classe Rectangle extensible

Ajoutons open à la classe Rectangle pour autoriser l'héritage.

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

??? class Rectangle(var width: Int, var height: Int)

👉 Question 1 : Implémentation de la classe Square

Complétez le code suivant en remplaçant les ?? par les bonnes instructions :

class Square(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 : violation du LSP

Si une instance de Square est utilisée là où un Rectangle est attendu, des comportements incohérents peuvent apparaître.

Exemple de dysfonctionnement

Imaginons une fonction utilitaire qui double la taille d'un rectangle :

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

📝 Écrivez le code permettant de doubler la taille d'un rectangle.

Que constatez-vous ?

👉 Question 2 : Test du comportement avec un carré

Essayons d'appliquer cette fonction à un carré :

val square = Square(3)
println(square.getArea())
doubleTheSizeOf(square)
println(square.getArea())
  • Que constatez-vous ?
  • Pourquoi ce comportement se produit-il ?

Le problème vient du fait que width et height sont indépendants dans Rectangle, alors qu'ils doivent être égaux dans Square. Cette contradiction entraîne un comportement inattendu.


Solution : Rendre les classes immuables

Une manière d'éviter cette incohérence est de rendre Rectangle et Square immuables, c'est-à-dire que leurs dimensions ne peuvent pas être modifiées après l'initialisation.

Cela signifie :

  • Supprimer les var au profit de val.
  • Ne permettre la modification des dimensions qu'en créant une nouvelle instance.

📝 Réécrivez le code pour que l'aire du rectangle et du carré soit correctement calculée sans violer le LSP.


Conclusion

Le principe de substitution de Liskov impose que les sous-classes respectent les contraintes de leurs classes parentes sans modifier leur comportement attendu. Dans cet exemple :

  • Hériter directement Square de Rectangle est une mauvaise idée, car un carré impose une contrainte que Rectangle ne possède pas.
  • Une solution alternative est d'utiliser une hiérarchie différente où Rectangle et Square ne sont pas liés par héritage mais implémentent une même interface ou utilisent la composition au lieu de l'héritage.

Résumé

  • Le Liskov Substitution Principle (LSP) garantit que les sous-classes peuvent remplacer leurs classes parentes sans provoquer d’effets indésirables.
  • Un carré ne peut pas être un sous-type d’un rectangle en raison de ses contraintes spécifiques.
  • Une meilleure approche est d'utiliser la composition ou des interfaces pour éviter la violation du LSP.

📝 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment