LSP, композиция vs наследование
LSP (принцип подстановки Лисков) — наследник должен полностью заменять родителя, не ломая логику программы. Если код работает с Animal, он обязан так же корректно работать с любым Dog extends Animal, без сюрпризов.
Классический пример нарушения — «Квадрат не должен наследоваться от Прямоугольника»:
class Rectangle {
int width, height;
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
}
class Square extends Rectangle {
void setWidth(int w) { width = w; height = w; } // ломает ожидания!
void setHeight(int h) { width = h; height = h; }
}
Код, который ожидает, что setWidth меняет только ширину прямоугольника, сломается на Square — это и есть нарушение LSP: наследник изменил поведение, а не просто расширил его.
Копнуть глубже
Композиция vs наследование — что выбрать. Наследование — это связь «является» (Dog is a Animal), композиция — связь «содержит» (Car has a Engine):
// Наследование: Car IS-A Vehicle
class Car extends Vehicle { ... }
// Композиция: Car HAS-A Engine
class Car {
private Engine engine; // Car содержит Engine, а не "является" им
}
Правило индустрии: «предпочитай композицию наследованию». Наследование жёстко связывает классы навсегда (наследник зависит от всех деталей реализации родителя), а композицию легко поменять в рантайме — просто подставить другой объект.
Под капотом
Почему наследование считается «хрупким» (fragile base class problem): изменение родительского класса может незаметно сломать всех наследников, даже если ты не трогал их код напрямую. Метод родителя, на который никто не рассчитывал как на «расширяемый», может оказаться переопределён в самых неожиданных местах — и любое изменение его поведения каскадом ломает иерархию.
Композиция этой проблемы не имеет: объект знает только публичный интерфейс того, что он использует, а не его внутреннее устройство — изменения внутри не пробивают эту границу.
• чем композиция отличается от наследования и почему её часто предпочитают (если дошёл до 2-го слоя).