Принцип подстановки Барбары Лисков гласит:

Объекты в программе должны быть заменяемыми на экземпляры их подклассов без изменения правильности работы программы.

Простыми словами: класс-наследник (дочерний класс) должен расширять поведение базового класса (родителя), но ни в коем случае не ломать его. Если вы вместо родительского класса передадите в функцию любой из его подклассов, программа должна продолжить работать корректно и без ошибок.


❌ Нарушение принципа (плохой код)

Классический пример нарушения LSP — это классическая геометрическая задача с квадратом и прямоугольником. С точки зрения математики квадрат — это прямоугольник. Но в программировании наследование квадрата от прямоугольника ломает логику.

class Rectangle {
  constructor(protected width: number, protected height: number) {}

  public setWidth(width: number) { this.width = width; }
  public setHeight(height: number) { this.height = height; }
  
  public getArea(): number {
    return this.width * this.height;
  }
}

// Квадрат наследуется от Прямоугольника
class Square extends Rectangle {
  // Нарушение LSP: Квадрат вынужден менять поведение родительских методов,
  // так как у него стороны должны быть равны. Изменение одной стороны меняет обе.
  public setWidth(width: number) {
    this.width = width;
    this.height = width;
  }

  public setHeight(height: number) {
    this.width = height;
    this.height = height;
  }
}

// Функция, которая ожидает Прямоугольник
function renderArea(rectangle: Rectangle) {
  rectangle.setWidth(4);
  rectangle.setHeight(5);
  
  // Для ЛЮБОГО прямоугольника площадь 4 * 5 должна быть 20.
  console.log(`Ожидаемая площадь: 20, Реальная: ${rectangle.getArea()}`);
}

// Использование:
renderArea(new Rectangle(0, 0)); // Выведет 20. Всё супер!
renderArea(new Square(0, 0));    // Выведет 25! Логика функции сломалась из-за подкласса.

Почему это плохо? Разработчик функции renderArea рассчитывает на стандартное поведение прямоугольника. Но подкласс Square неожиданно изменил правила игры, нарушив контракт родительского класса.


✅ Соблюдение принципа (хороший код)

Квадрат и прямоугольник — это разные фигуры со своими правилами. Вместо того чтобы наследовать их друг от друга, их нужно объединить под общей абстракцией (интерфейсом) Shape, где у каждого будет своя независимая реализация подсчета площади.

1. Создаем интерфейс для всех фигур

interface Shape {
  getArea(): number;
}

2. Создаем независимые классы

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  public setWidth(width: number) { this.width = width; }
  public setHeight(height: number) { this.height = height; }

  public getArea(): number {
    return this.width * this.height;
  }
}

class Square implements Shape {
  constructor(private size: number) {}

  public setSize(size: number) { this.size = size; }

  public getArea(): number {
    return this.size * this.size;
  }
}

3. Используем полиморфизм без скрытых багов

Теперь функция работает с абстракцией Shape. Мы можем передать туда и Rectangle, и Square — они гарантированно вернут правильную площадь, не ломая ожидания разработчика.

function printArea(shape: Shape) {
  // Функция просто просит фигуру посчитать площадь. Никаких скрытых сюрпризов.
  console.log(`Площадь фигуры: ${shape.getArea()}`);
}

// Использование:
const rect = new Rectangle(4, 5);
const square = new Square(5);

printArea(rect);   // Выведет: 20
printArea(square); // Выведет: 25

Как понять, что вы нарушаете LSP?

  1. В дочернем классе есть методы, которые выбрасывают ошибку вроде throw new Error("Этот метод не поддерживается").
  2. Дочерний класс полностью игнорирует или зануляет логику родительского метода.
  3. Вам приходится использовать проверку типов (if (figura instanceof Square)) внутри функций, которые работают с родительским классом.