SOLID : Liskov Substitution Principle (LSP)
Objectif : Les sous-classes doivent pouvoir remplacer leur classe de base sans changer le comportement attendu.
Le Principe de Substitution de Liskov est l'un des cinq principes SOLID de la programmation orientée objet.
Formulé par Barbara Liskov en 1987, il stipule que :
"Si S est un sous-type de T, alors les objets de type T peuvent être remplacés par des objets de type S sans altérer les propriétés désirables du programme."
Caractéristiques clés du LSP
- Les sous-classes doivent respecter les contrats établis par les classes parentes
- Les sous-classes peuvent étendre les fonctionnalités mais ne doivent pas les restreindre
- Les méthodes surchargées ne doivent pas renforcer les préconditions ni affaiblir les postconditions
Anti-pattern
# Exemple initial qui viole le LSP
class Rectangle
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
@width * @height
end
end
class Square < Rectangle
def width=(value)
@width = value
@height = value # Modification de height quand width change
end
def height=(value)
@width = value # Modification de width quand height change
@height = value
end
end
# Code client qui utilise ces classes
def print_area_after_resize(rectangle)
rectangle.width = 5
rectangle.height = 4
# On s'attend à ce que l'aire soit 5 * 4 = 20
puts "L'aire attendue est 20, l'aire calculée est #{rectangle.area}"
end
# Test avec Rectangle
rect = Rectangle.new(3, 3)
print_area_after_resize(rect) # Affiche: L'aire attendue est 20, l'aire calculée est 20
# Test avec Square
square = Square.new(3, 3)
print_area_after_resize(square) # Affiche: L'aire attendue est 20, l'aire calculée est 16
Dans cet exemple, Square
viole le LSP car il modifie le comportement attendu de Rectangle
. Quand on définit la largeur puis la hauteur d'un carré, la dernière opération écrase la précédente, ce qui n'est pas le comportement attendu d'un rectangle.
Pattern
# Solution respectant le LSP
class Shape
def area
raise NotImplementedError, "Les sous-classes doivent implémenter cette méthode"
end
end
class Rectangle < Shape
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
@width * @height
end
end
class Square < Shape
attr_accessor :side
def initialize(side)
@side = side
end
def area
@side * @side
end
end
# Code client adapté
def print_area(shape)
puts "L'aire de la forme est #{shape.area}"
end
# Test avec Rectangle
rect = Rectangle.new(5, 4)
print_area(rect) # Affiche: L'aire de la forme est 20
# Test avec Square
square = Square.new(4)
print_area(square) # Affiche: L'aire de la forme est 16
Dans cette solution, nous avons :
- Créé une classe de base
Shape
avec une méthodearea
abstraite - Fait hériter
Rectangle
etSquare
directement deShape
- Implémenté des comportements spécifiques pour chaque forme
- Modifié le code client pour qu'il n'ait pas d'attentes spécifiques sur la façon de modifier les dimensions