Liskov Substitution Principle
Liskov Substitution Principle (LSP) — третий принцип SOLID: объект подкласса должен быть взаимозаменяем с объектом базового класса, не нарушая корректности программы.
Зачем нужно
Нарушение LSP приводит к хрупкому коду, где наследование ломает контракт базового класса. Распознав нарушение LSP, разработчик понимает, что иерархия классов построена неверно, и перепроектирует с использованием composition или интерфейсов. Принцип гарантирует, что полиморфизм работает предсказуемо.
Где используется
- Проектирование иерархий классов
- Полиморфные коллекции (массив объектов разных типов с одним интерфейсом)
- Проверка корректности наследования при рефакторинге
- TypeScript: проверка типов при наследовании интерфейсов
Основной контент
Классический пример нарушения LSP
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w) {
this.width = w;
this.height = w; // квадрат всегда одинаков
}
setHeight(h) {
this.width = h;
this.height = h;
}
}
// Функция, работающая с Rectangle
function testRectangle(rect) {
rect.setWidth(5);
rect.setHeight(4);
console.log(rect.area); // ожидаем 20
}
testRectangle(new Rectangle); // 20 — верно
testRectangle(new Square); // 16 — нарушение! Square не заменяет Rectangle
Правильный подход: composition вместо наследования
// Интерфейс Shape — без наследования
class Shape {
area { throw new Error('Не реализовано'); }
}
class Rectangle extends Shape {
constructor(w, h) { super; this.w = w; this.h = h; }
area { return this.w * this.h; }
}
class Square extends Shape {
constructor(side) { super; this.side = side; }
area { return this.side * this.side; }
}
// Теперь оба корректно заменяют Shape
function printArea(shape) {
console.log(`Площадь: ${shape.area}`);
}
printArea(new Rectangle(5, 4)); // 20
printArea(new Square(4)); // 16
LSP и переопределение методов
class Bird {
move { return 'летит'; }
}
class Penguin extends Bird {
move { return 'идёт'; } // OK: переопределяет, но не нарушает контракт
}
// Нарушение: метод выбрасывает ошибку вместо поведения
class OstrichBad extends Bird {
move { throw new Error('Страус не летает!'); }
}
// Корректная обработка
const birds = [new Bird, new Penguin];
birds.forEach(b => console.log(b.move)); // работает для всех
Частые ошибки
- Наследование ради переиспользования кода — если подкласс переопределяет методы базового класса с другой семантикой, это нарушение LSP. Используйте composition.
- Выбрасывание исключений в переопределённых методах —
throw new Error('Not implemented')нарушает контракт. Пересмотрите иерархию. - Сужение предусловий или расширение постусловий — подкласс не должен требовать больше, чем базовый, и возвращать меньше.
🎓 Источники
- 🎓 [SOLID Liskov Substitution Principle — LSP for JavaScript] · 2019-11-07 · YouTube
- Тезисы: LSP не только про наследников — это и про объекты, реализующие контракт (duck typing). LSP вытекает из OCP. Главный сигнал нарушения: появление
if (x instanceof Square)— это лапша. В JS подстановка работает по интерфейсу/структурной типизации. - «Самое главное, что в принципе подстановки не обязательно наследники, а какие-то объекты, реализующие контракт. Вот так вот это уже нам будет ближе к JavaScript».
- Альтернативная позиция: LSP оказывается простым, «страшная аура вокруг принципа подстановки рассосалась».
- Тезисы: LSP не только про наследников — это и про объекты, реализующие контракт (duck typing). LSP вытекает из OCP. Главный сигнал нарушения: появление