Ответ
Принцип подстановки Лисков (LSP) — это один из принципов SOLID. Он гласит, что объекты базового класса должны быть заменяемыми объектами его подклассов без изменения корректности программы. Нарушение LSP ведет к хрупкости кода и неожиданным ошибкам.
Сложности соблюдения LSP: Соблюдать LSP сложно, когда наследование используется для повторного использования кода, а не для выражения истинного отношения "является" (is-a).
Классический пример нарушения LSP:
class Rectangle {
private int width;
private int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // Нарушение контракта Rectangle!
}
// Аналогично переопределяется setHeight
}
Клиентский код, ожидающий, что setWidth и setHeight работают независимо, сломается при работе с Square.
Как соблюдать LSP на практике:
- Четкий контракт: Базовый класс должен явно или неявно определять инварианты, пред- и постусловия для своих методов.
- Предпочитать композицию: Если семантика подкласса отличается, лучше использовать композицию (
SquareсодержитRectangle). - Избегать усиления предусловий и ослабления постусловий в подклассах.
- Не изменять типы возвращаемых значений или исключений непредсказуемым образом.
Соблюдение LSP требует тщательного проектирования иерархий, но значительно упрощает расширение и поддержку системы.
Ответ 18+ 🔞
Смотри, есть такая штука — принцип подстановки Лисков. Один из этих ваших SOLID-принципов, которые все так любят на собеседованиях спрашивать, а потом в коде нихуя не соблюдают. Суть в чём: если у тебя есть какой-то базовый класс, то ты должен иметь возможность подсунуть вместо него любой его подкласс, и программа не должна от этого взорваться или начать творить какую-то дичь. Всё должно работать, как и задумано.
А сложность-то вся в чём? Да в том, что народ начинает наследование использовать как дубинку, чтобы просто код переиспользовать, а не потому что там реальное отношение «является». Вот тут и начинается пиздец.
Сейчас я тебе классику жанра покажу, от которой у любого адекватного разработчика волосы дыбом встают. Смотри, есть у нас Rectangle — прямоугольник.
class Rectangle {
private int width;
private int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
Всё логично, ширина, высота, площадь. А теперь какой-то умник решил, что квадрат — это частный случай прямоугольника, и наследует от него Square.
class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // Нарушение контракта Rectangle!
}
// Аналогично переопределяется setHeight
}
И что мы имеем? А имеем мы, блядь, полный пиздец! Клиентский код, который работает с Rectangle и ожидает, что setWidth и setHeight меняют только свою сторону, ломается в хлам. Он же не знает, что ему подсунули эту мартышлюшку-квадрат, который при изменении ширины ещё и высоту под себя подгоняет! Это же нарушение контракта в чистом виде, ёпта! Вот тебе и «является». С математической точки зрения — да, квадрат является прямоугольником. А с поведенческой — нихуя подобного, это две разные сущности с разными правилами игры.
Так как же не наступать на эти грабли?
- Чёткий контракт, блядь! Базовый класс должен ясно говорить (или хотя бы подразумевать), что можно, а что нельзя. Какие инварианты, какие предусловия у методов. Если в базовом классе
setWidthне трогает высоту, то и в наследниках нехуй её трогать. - Композиция, а не наследование, когда есть сомнения. Может, нашему квадрату не нужно наследоваться от прямоугольника? Может, ему лучше содержать прямоугольник и управлять им по-своему? Часто это куда более безопасный путь.
- Не усиливать предусловия и не ослаблять постусловия в потомках. Это как если бы базовый класс говорил: «Принеси мне любой напиток». А наследник уточняет: «Только не газировку!». Или базовый класс гарантирует: «Верну тебе число». А наследник вдруг начинает возвращать
null. Вот это всё — прямой путь в ад и к ночным вызовам на прод. - Не менять типы возвращаемых значений и исключения как попало. Если уж меняешь, то только на более специфичные (ковариантные), и то с оглядкой.
В общем, соблюдение LSP — это не про тупое следование правилам. Это про здравый смысл и проектирование иерархий, которые не выстрелят тебе в ногу, когда ты будешь их расширять. Потрать время на дизайн, чтобы потом не орать «ёбаный насос!» посреди ночи, разбираясь, почему всё сломалось.