Liskov Substitution Principle (LSP) — принцип подстановки Барбары Лисков
Принцип подстановки Лисков (Liskov Substitution Principle, LSP) говорит о том, что объекты дочерних классов должны полностью заменять объекты родительского класса без изменения корректности работы программы.
Если ещё проще:
Если класс B наследуется от класса A, он должен вести себя так, чтобы его можно было подставить туда, где ожидается A — и ничего не ломалось.
LSP помогает избежать ситуаций, когда наследник ведёт себя неожиданно, нарушает ожидания программы или изменяет контракт родителя.
Зачем нужен LSP?
1. Предсказуемость поведения
Код, который использует родительский класс, должен одинаково работать и с потомком. Если это не так — архитектура становится хрупкой.
2. Корректная работа полиморфизма
Полиморфизм — это круто. Но он работает только тогда, когда контракт не нарушается.
3. Чистая архитектура и меньше багов
Нарушения LSP часто приводят к «магическим» ошибкам, когда часть системы работает, а часть — нет, хотя используется один и тот же интерфейс.
Пример нарушения LSP
Допустим, мы создаём систему для геометрических фигур:
class Rectangle
{
protected float $width;
protected float $height;
public function setWidth(float $width): void
{
$this->width = $width;
}
public function setHeight(float $height): void
{
$this->height = $height;
}
public function getArea(): float
{
return $this->width * $this->height;
}
}
Теперь создадим квадрат:
class Square extends Rectangle
{
public function setWidth(float $width): void
{
$this->width = $width;
$this->height = $width; // квадрат делает стороны равными
}
public function setHeight(float $height): void
{
$this->width = $height;
$this->height = $height;
}
}
На первый взгляд вроде всё нормально. Но вот что произойдёт:
function calcArea(Rectangle $rect)
{
$rect->setWidth(4);
$rect->setHeight(5);
return $rect->getArea(); // ожидаем 20
}
echo calcArea(new Rectangle()); // 20
echo calcArea(new Square()); // 25 — ошибка!
Квадрат нарушает контракт родителя.
Он меняет логику так, что код перестаёт быть предсказуемым
Как исправить?
Правильное решение: не делать Square наследником Rectangle.
Они действительно похожи, но логически они не подходят друг другу.
Создадим общий интерфейс:
interface Shape
{
public function getArea(): float;
}
Класс прямоугольника:
class Rectangle implements Shape
{
public function __construct(
private float $width,
private float $height
) {}
public function getArea(): float
{
return $this->width * $this->height;
}
}
Класс квадрата:
class Square implements Shape
{
public function __construct(
private float $side
) {}
public function getArea(): float
{
return $this->side * $this->side;
}
}
Теперь поведение корректное, а интерфейс работает стабильно.
Где в реальности встречаются нарушения LSP?
Чаще всего — в следующих ситуациях:
- неправильное наследование для “похожих, но разных” сущностей
- методы потомков выбрасывают исключения там, где родитель не должен
- потомок меняет смысл параметров
- потомок нарушает ограничения родительского класса
- наследование используется “потому что так проще”
LSP помогает понять, когда нужно использовать интерфейсы или композицию вместо наследования.
Резюме
- LSP говорит: потомок должен полностью соответствовать контракту родителя.
- Если класс не может вести себя как родитель — он не должен от него наследоваться.
- Часто решение — заменить наследование интерфейсом или композицией.
- Соблюдение LSP делает систему предсказуемой, гибкой и устойчивой к изменениям.