Как неправильное наследование может нарушить Принцип подстановки Лисков (LSP)?

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

Ответ

Главная проблема — нарушение Принципа подстановки Лисков (Liskov Substitution Principle, LSP). Этот принцип гласит, что объекты дочерних классов должны быть способны заменять объекты родительского класса без изменения корректности программы.

Нарушение LSP приводит к непредсказуемому поведению, усложняет поддержку и создаёт хрупкую иерархию классов.

Классический пример: Проблема "Квадрат-Прямоугольник"

Логично предположить, что Квадрат является частным случаем Прямоугольника и может от него наследоваться. Однако это нарушает LSP.

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

    def get_width(self):
        return self._width

    def set_width(self, width):
        self._width = width

    def get_height(self):
        return self._height

    def set_height(self, height):
        self._height = height

    def area(self):
        return self._width * self._height

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

    # Переопределяем сеттеры, чтобы сохранить инвариант квадрата (ширина == высота)
    def set_width(self, width):
        self._width = width
        self._height = width # Нарушение LSP: изменение ширины неожиданно меняет и высоту

    def set_height(self, height):
        self._width = height
        self._height = height # Нарушение LSP: изменение высоты неожиданно меняет и ширину

Почему это проблема?

Рассмотрим функцию, которая работает с Rectangle и ожидает, что изменение ширины не затронет высоту:

def use_rectangle(rect: Rectangle):
    rect.set_width(20)
    rect.set_height(10)
    # Ожидаем площадь 200 (20 * 10)
    # Но для Square площадь будет 100 (10 * 10), т.к. set_height изменил и ширину
    assert rect.area() == 200, "Behavior is broken!"

r = Rectangle(5, 5)
use_rectangle(r) # OK

s = Square(5)
use_rectangle(s) # AssertionError: Behavior is broken!

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