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
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 leverE
ou un sous-type deE
).
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
.
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)
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.
Si une instance de Square
est utilisée là où un Rectangle
est attendu, des comportements incohérents peuvent apparaître.
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 ?
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.
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.
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
deRectangle
est une mauvaise idée, car un carré impose une contrainte queRectangle
ne possède pas. - Une solution alternative est d'utiliser une hiérarchie différente où
Rectangle
etSquare
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.
- 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.