Пример нарушения принципа подстановки Лисков (LSP) в ООП

«Пример нарушения принципа подстановки Лисков (LSP) в ООП» — вопрос из категории ООП, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Принцип подстановки Лисков (LSP) гласит, что объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. Нарушение LSP происходит, когда подкласс изменяет ожидаемое поведение базового класса, делая его несовместимым с кодом, который работает с базовым типом.

Пример нарушения LSP (Python):

Рассмотрим классический пример с Rectangle и Square:

class Rectangle:
    def __init__(self, width: int, height: int):
        self._width = width
        self._height = height

    @property
    def width(self) -> int:
        return self._width

    @width.setter
    def width(self, value: int):
        self._width = value

    @property
    def height(self) -> int:
        return self._height

    @height.setter
    def height(self, value: int):
        self._height = value

    def area(self) -> int:
        return self._width * self._height

class Square(Rectangle):
    def __init__(self, side: int):
        super().__init__(side, side)

    @Rectangle.width.setter
    def width(self, value: int):
        # Нарушение: изменение ширины влияет на высоту
        self._width = value
        self._height = value

    @Rectangle.height.setter
    def height(self, value: int):
        # Нарушение: изменение высоты влияет на ширину
        self._width = value
        self._height = value

def test_rectangle_area(rect: Rectangle):
    # Ожидаем, что изменение ширины не повлияет на высоту
    rect.width = 5
    rect.height = 4
    # Для Rectangle: 5 * 4 = 20
    # Для Square: 5 * 5 = 25 (после rect.width = 5), затем 4 * 4 = 16 (после rect.height = 4)
    assert rect.area() == 20  # Это утверждение будет ложным для Square!

# Тестирование с Rectangle - работает корректно
rectangle_obj = Rectangle(10, 2)
test_rectangle_area(rectangle_obj) # OK, area = 20

# Тестирование с Square - нарушает LSP
square_obj = Square(10)
try:
    test_rectangle_area(square_obj) # AssertionError
except AssertionError:
    print("Нарушение LSP: Square не может быть подставлен вместо Rectangle без изменения поведения.")

Объяснение нарушения:

Класс Square наследуется от Rectangle, но изменяет его фундаментальное поведение: при изменении ширины или высоты Square обе стороны устанавливаются одинаковыми. Это нарушает инвариант Rectangle, который предполагает независимое изменение сторон.

Функция test_rectangle_area, разработанная для работы с Rectangle, ожидает, что установка ширины и высоты будет работать независимо. При подстановке Square это ожидание нарушается, что приводит к некорректным результатам (assert rect.area() == 20 становится ложным, так как для Square площадь будет 16).

Почему это проблема и как исправить:

Нарушение LSP приводит к хрупкости кода, затрудняет его поддержку и расширение, так как подклассы не могут быть безопасно использованы там, где ожидается базовый класс. Правильным решением было бы не наследовать Square от Rectangle, поскольку Square не является 'специальным видом' Rectangle в контексте независимых размеров. Вместо этого можно использовать:

  • Общий интерфейс (абстрактный базовый класс): Определить общий интерфейс Shape с методом area(), от которого будут наследоваться как Rectangle, так и Square.
  • Композиция: Square может содержать Rectangle как внутренний компонент, управляя его размерами.