В чём суть принципа подстановки Барбары Лисков (LSP)?

Ответ

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

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

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

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width } // Побочный эффект: изменение width меняет height
    }
    override var height: Double {
        didSet { width = height }
    }
}

func resizeRectangle(_ rectangle: Rectangle) {
    rectangle.width = 5
    rectangle.height = 4
    print("Ожидаемая площадь: 20. Фактическая: (rectangle.area)")
}

let square = Square()
resizeRectangle(square) // Напечатает: Ожидаемая площадь: 20. Фактическая: 16.0
// Поведение изменилось! Это нарушение LSP.

Следствия и правила LSP:

  • Предусловия (требования к входным данным) в подклассе не могут быть строже.
  • Постусловия (гарантии на выходе) в подклассе не могут быть слабее.
  • Инварианты базового класса (условия, всегда истинные) должны сохраняться в подклассе.
  • Подкласс не должен генерировать новых типов исключений, не описанных в базовом классе.

Цель: Создавать корректные иерархии наследования, где подтипы являются настоящими специализациями базового типа, а не просто имеют схожий код.

Ответ 18+ 🔞

А, слушай, про этот ваш принцип Лисков, блядь! Это ж та самая история, где если ты подсовываешь наследника вместо родителя, а программа ломается — значит ты, сука, накосячил с архитектурой, и твоё наследование — это пиздопроебибна полная!

Короче, представь: есть у тебя функция, которая работает с неким «Устройством». И ты спокойно можешь запихнуть туда «Принтер», «Сканер» или «Ксерокс». И всё работает, ёпта! Потому что ксерокс — он и есть устройство, только с доп. фичами. А вот если ты попытаешься сунуть туда «Кофеварку», которая тоже в твоей ебалой иерархии наследуется от «Устройства», потому что у неё тоже кнопка есть — вот тут-то и начнётся пиздец. Функция попробует отправить документ на печать, а кофеварка, блядь, нальёт тебе американо в лоток для бумаги. Это и есть нарушение, понимаешь? Наследник должен делать ВСЁ, что умеет родитель, и даже больше, но не менять суть, блядь!

Вот тебе классика жанра, от которой все учебники обоссались. Прямоугольник и квадрат.

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width } // А вот и хуйня! Меняешь ширину — за собой и высоту тащит!
    }
    override var height: Double {
        didSet { width = height }
    }
}

Смотри, вроде логично: квадрат — частный случай прямоугольника. Ан нет, блядь! Берём простую функцию, которая работает с прямоугольником:

func resizeRectangle(_ rectangle: Rectangle) {
    rectangle.width = 5
    rectangle.height = 4
    print("Ожидаемая площадь: 20. Фактическая: (rectangle.area)")
}

let square = Square()
resizeRectangle(square) // Напечатает: Ожидаемая площадь: 20. Фактическая: 16.0

Вот тебе и волнение, ебать! Функция-то ожидает, что ширина и высота живут отдельной жизнью, а этот ёбаный квадрат-умник связывает их намертво. Получили 16 вместо 20. Программа не сломалась, но поведение изменилось пиздец как! Это и есть нарушение LSP в чистом виде. Квадрат НЕ является заменой прямоугольника в этой модели, хоть ты тресни.

Отсюда вытекают, блядь, железные правила, которые нарушать — себя не уважать:

  • Предусловия (требования на вход) в наследнике нельзя ужесточать, блядь. Нельзя говорить: «Папаша принимал любые Int, а я, сынок, буду только положительные». Это пиздец, клиент сломается.
  • Постусловия (обещания на выходе) в наследнике нельзя ослаблять. Если папаша клялся, что вернёт не-nil, то сынуля не имеет права вдруг начать возвращать nil. Иначе все, кто на папу рассчитывал, полетят в тартарары.
  • Инварианты (вечные истины) родителя должны жить и в потомке. Если у «Птицы» инвариант «может летать», то страус, блядь, не должен от неё наследоваться. Иначе функция устроить_полёт(птица) отправит страуса на верную смерть.
  • И самое смешное — исключения. Наследник не имеет права выкидывать новые, неведомые родителю исключения. Все только те, что объявлены у предка, или их подтипы. А то обложился ты try-catch'ами, а тут — хуй! — новый эксепшн из глубин иерархии.

Итог, ёпта: LSP — это не про синтаксис, это про логику, про семантику, блядь. Наследование — это про отношение «является» в самом строгом смысле. Не «похож», а именно «является». Иначе получается хитрая жопа, которая выглядит как утка, крякает как утка, но когда ты пытаешься её зажарить, она взрывается. Вот это и есть архитектурный пиздец.