Что такое принцип подстановки Барбары Лисков (LSP)?

Ответ

Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) — это один из пяти принципов SOLID. Он гласит, что объекты производного (дочернего) класса должны быть способны заменять объекты базового (родительского) класса без изменения корректности работы программы.

Простыми словами: если у вас есть функция, которая работает с классом A, она должна так же корректно работать с любым его подклассом B, не зная о существовании B.

Почему это важно? LSP гарантирует, что иерархия наследования является семантически правильной. Подкласс должен расширять, а не изменять поведение базового класса.

Классический пример нарушения LSP (проблема квадрата и прямоугольника):

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

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

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

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

# Квадрат — это частный случай прямоугольника, но...
class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

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

    # Аналогичное нарушение
    def set_height(self, height):
        self._width = height
        self._height = height

# Функция, которая ожидает поведение Rectangle
def process_rectangle(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(10)
    # Ожидаем площадь 5 * 10 = 50
    print(f"Ожидаемая площадь: 50, Фактическая: {rect.get_area()}")

# С базовым классом всё работает
process_rectangle(Rectangle(2, 3)) # Фактическая: 50

# С подклассом поведение нарушается
process_rectangle(Square(2))      # Фактическая: 100 (т.к. set_height изменила и ширину)

Этот код нарушает LSP, потому что Square изменяет инвариант (независимость ширины и высоты), установленный в Rectangle. Это приводит к непредсказуемым результатам.

Ответ 18+ 🔞

Давай я тебе на пальцах объясню, что за принцип подстановки Лисков такой, а то звучит, как диагноз, блядь. Представь, что у тебя есть инструкция «Как пользоваться дверью». Ты пишешь её для обычной двери, а потом приходит какой-то умник и говорит: «А вот у меня дверь-вертолёт, она тоже дверь, по идее!». И начинает её по твоей инструкции открывать, а она, сука, взлетает и уносит ему крышу. Вот это и есть нарушение принципа, ёпта.

Короче, суть: если у тебя есть код, который работает с классом Папа, то он должен так же спокойно, без сюрпризов, работать и с любым его классом-сыном Сын. А если сын начинает выёбываться и менять правила игры — это пиздец, нарушение.

Зачем это нужно, блядь? Чтобы твоя иерархия классов не была сборищем психопатов, где каждый наследник творит, что хочет. Подкласс должен дополнять поведение, а не ломать ожидания от родителя. Иначе потом будешь искать баги, как иголку в стоге сена, да ещё и с сюрпризом в виде вертолётной двери в жопу.

Вот тебе классический пример, от которого у всех программистов лицо в гримассе, как от лимона:

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

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

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

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

# А теперь смотри сюда, начинается магия, блядь, или её полное отсутствие.
# Квадрат — он же прямоугольник, да? Ну логично, наследуемся!
class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

    # А вот тут, сука, собака и зарыта. Начинаем переопределять методы.
    def set_width(self, width):
        self._width = width
        self._height = width  # Ой, ёбта! А это что за хуйня? У прямоугольника так не должно быть!

    def set_height(self, height):
        self._width = height
        self._height = height # Да тут та же самая хрень, только сбоку!

# Теперь функция, которая думает, что работает с нормальным прямоугольником.
def process_rectangle(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(10)
    # Ну ясень пень, площадь будет 5 * 10 = 50. Логика же железная.
    print(f"Ожидаемая площадь: 50, Фактическая: {rect.get_area()}")

# С нормальным прямоугольником всё пучком.
process_rectangle(Rectangle(2, 3)) # Выведет 50, как и заказывали.

# А теперь подсовываем нашего «особенного» квадрата.
process_rectangle(Square(2))      # А тут, мать его, 100! Потому что set_height(10) заодно и ширину в 10 превратила!

Вот видишь, в чём подвох? Код process_rectangle ожидает, что ширина и высота меняются независимо — это контракт класса Rectangle. А его сынок Square этот контракт, простите за выражение, в рот берёт и нарушает. В итоге программа работает не так, как задумано. Это и есть нарушение принципа Лисков, и оно, блядь, приводит к таким вот ебабельно тонким и сложным для отлова ошибкам.

Так что запомни, чувак: наследование — это не про «он вроде похож», а про «он точно ведёт себя так же, только, может, чуть больше умеет». Если не можешь этого гарантировать — не наследуйся, иди другим путём, блядь (композицией, например). А то получишь архитектурный пиздец, в рот меня чих-пых.