SOLID : Liskov Substitution Principle (LSP)

code 9 juil. 2025

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

  1. Les sous-classes doivent respecter les contrats établis par les classes parentes
  2. Les sous-classes peuvent étendre les fonctionnalités mais ne doivent pas les restreindre
  3. 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 :

  1. Créé une classe de base Shape avec une méthode area abstraite
  2. Fait hériter Rectangle et Square directement de Shape
  3. Implémenté des comportements spécifiques pour chaque forme
  4. Modifié le code client pour qu'il n'ait pas d'attentes spécifiques sur la façon de modifier les dimensions

Mots clés

Romain GEORGES

Open Source evangelist & Ruby enthousiast