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 оказывается простым, «страшная аура вокруг принципа подстановки рассосалась».

Связанные темы

Ресурсы