Ответ
С корректным использованием наследования напрямую связан принцип Liskov Substitution Principle (LSP) — "Принцип подстановки Барбары Лисков".
Формулировка: Объекты базового класса должны быть заменяемыми объектами его подклассов без изменения корректности программы. Наследующий класс должен дополнять, а не изменять поведение родителя.
Суть принципа: LSP устанавливает строгие правила для наследования. Если класс S является подтипом класса T, то везде, где используется T, можно безопасно использовать S, не ломая логику.
Пример нарушения LSP:
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
// Квадрат «является» прямоугольником с математической точки зрения,
// но с точки зрения поведения — нет.
class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // Нарушение: изменение высоты при установке ширины
}
@Override
public void setHeight(int h) {
super.setHeight(h);
super.setWidth(h); // Нарушение: изменение ширины при установке высоты
}
}
// Код, который работает с Rectangle, сломается для Square
void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Утверждение провалится для Square (area=16)
}
Вывод: LSP требует, чтобы подклассы были семантически совместимы со своими суперклассами, а не только синтаксически. Наследование должно означать "является" в строгом поведенческом смысле.
Ответ 18+ 🔞
А, ну вот, подъехали мы к самому сочному, блядь! Принцип подстановки Лисков, ёпта! Это та самая Барбара, которая влепила нам по мозгам, что наследование — это вам не хуй собачий, а серьёзная хуйня.
Короче, суть в чём: если у тебя есть какой-то базовый класс, типа Утка, то его наследник, например, РезиноваяУтка, должен подходить везде, где используется Утка. И программа при этом не должна превращаться в пиздец. То есть, подставляешь наследника — и всё должно работать, как часы, а не как говно в проруби.
А теперь смотри, где все обламываются. Берём классический пример, который всех вгоняет в ступор.
Допустим, есть у нас класс Прямоугольник. Ну, прямоугольник и прямоугольник, хуле. Ширина, высота, площадь.
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
И тут приходит какой-то умник и говорит: «А давайте сделаем класс Квадрат! Он же является прямоугольником, похуй!». И тупо наследует.
class Square extends Rectangle {
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // ОЙ, БЛЯДЬ! А ЧЁ ЭТО ТЫ ТУТ ДЕЛАЕШЬ?
}
@Override
public void setHeight(int h) {
super.setHeight(h);
super.setWidth(h); // ДА ТЫ ЧЁ, СОВСЕМ ОХУЕЛ?!
}
}
И вроде бы логично, ёпта: у квадрата ширина всегда равна высоте. Но с точки зрения поведения — это пиздец, извините. Смотри, что происходит.
Есть у нас функция, которая работает с любым Прямоугольником. Она выставляет ширину 5, высоту 4 и ожидает площадь 20. Логично же, да?
void testArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Упс, для Square тут будет 16! Пиздец, assertion failed!
}
И вот тут нас накрывает медным тазом. Подсунули мы Квадрат вместо Прямоугольника, а он, сука, ведёт себя не так! Он меняет вторую сторону при установке первой. Нарушается контракт, ожидания, вся хуйня. Программа летит в тартарары.
Вывод, блядь, какой? LSP — это про то, что наследование должно быть семантически правильным. Недосточно просто впихнуть extends и переопределить методы. Надо, чтобы наследник не ломал логику родителя. Чтобы его можно было подставить, и все остались живы-здоровы, а не охуели от неожиданного поведения.
Если наследник начинает делать какие-то ебушки-воробушки, которых от него не ждали, — это не наследование, а пиздопроебибна. Делай тогда композицию, блядь, и не позорься.