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

Ответ

Главная проблема — нарушение Принципа подстановки Лисков (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 нарушен. В таких случаях наследование является неверным выбором, и стоит рассмотреть композицию или другую архитектуру.

Ответ 18+ 🔞

А, слушай, смотри, тут такая дичь, блядь, классическая! Все думают: ну квадрат — он же прямоугольник, да? Ширина, высота, всё как у людей. Берём и наследуем, хуле. А потом — пиздец, программа летит в тартарары, и все сидят, чешут репу: "Почему?"

А причина-то, блядь, в нарушении Принципа подстановки Лисков (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  # Опа-на! Меняю ширину — заодно и высоту нахуярил!

    def set_height(self, height):
        self._width = height
        self._height = height  # И тут то же самое! Высоту поменял — и ширину за собой потянул!

Вроде бы, ну и что? Квадрат же, у него стороны равны, я инвариант сохраняю, молодец я, блядь. Ан нет! Смотри, что происходит дальше.

Допустим, есть у нас функция, которая работает с прямоугольником. Она ожидает, что ширина и высота — независимые хуйни, и меняет их по отдельности:

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

r = Rectangle(5, 5)
use_rectangle(r)  # Всё ок, работает, спасибо, до свидания.

s = Square(5)
use_rectangle(s)  # А тут — бабах! AssertionError! Программа в ахуе!

Почему? Да потому что наш "квадратик", сука, когда ему меняют высоту, он ещё и ширину под себя подминает! И наоборот. В итоге площадь получается не 200, а 100, и вся логика летит в пизду. Это и есть нарушение LSP в чистом виде: квадрат не может заменить прямоугольник, не сломав ожидаемого поведения.

Вывод, блядь, простой, как три копейки: не всякая "is-a" связь (квадрат — это прямоугольник) должна превращаться в наследование в коде. Иногда лучше взять композицию, чтобы не выстрелить себе в ногу, а потом орать "ой, бля, как же так вышло?". Вот так вот, ебать мои старые костыли, принципы SOLID — они не просто так придуманы, чтобы мозги нам пудрить.