Ответ
Принцип подстановки Лисков (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 — это не про синтаксис, это про логику, про семантику, блядь. Наследование — это про отношение «является» в самом строгом смысле. Не «похож», а именно «является». Иначе получается хитрая жопа, которая выглядит как утка, крякает как утка, но когда ты пытаешься её зажарить, она взрывается. Вот это и есть архитектурный пиздец.