Принцип подстановки Барбары Лисков гласит:
Объекты в программе должны быть заменяемыми на экземпляры их подклассов без изменения правильности работы программы.
Простыми словами: класс-наследник (дочерний класс) должен расширять поведение базового класса (родителя), но ни в коем случае не ломать его. Если вы вместо родительского класса передадите в функцию любой из его подклассов, программа должна продолжить работать корректно и без ошибок.
❌ Нарушение принципа (плохой код)
Классический пример нарушения 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?
- В дочернем классе есть методы, которые выбрасывают ошибку вроде
throw new Error("Этот метод не поддерживается"). - Дочерний класс полностью игнорирует или зануляет логику родительского метода.
- Вам приходится использовать проверку типов (
if (figura instanceof Square)) внутри функций, которые работают с родительским классом.