LSP («L» у SOLID): об'єкт підтипу має бути можливо підставити скрізь, де очікується базовий тип, без зміни правильності програми. Нащадок має виконувати контракт предка, а не лише мати ті самі методи.
Класичне порушення - квадрат і прямокутник:
class Rectangle
{
public function setWidth(int $w): void { $this->width = $w; }
public function setHeight(int $h): void { $this->height = $h; }
public function area(): int { return $this->width * $this->height; }
}
class Square extends Rectangle
{
public function setWidth(int $w): void { $this->width = $this->height = $w; }
public function setHeight(int $h): void { $this->width = $this->height = $h; }
}
function stretch(Rectangle $r): void
{
$r->setWidth(5);
$r->setHeight(2);
assert($r->area() === 10); // для Square - 4
}
Математично квадрат - прямокутник, але поведінково ні: незалежна зміна сторін - частина контракту Rectangle.
Як порушують у реальному коді:
- Метод нащадка кидає
NotImplementedExceptionчи нічого не робить:ReadOnlyRepository extends Repositoryзsave(), що кидає виняток. - Посилені вимоги до вхідних даних: предок приймає будь-який рядок, нащадок - лише непорожній.
- Послаблені гарантії результату: предок завжди повертає об'єкт, нащадок - інколи
null. - Нові винятки, яких клієнти базового типу не очікують і не ловлять.
- Перевірка типу в коді клієнта:
if ($shape instanceof Square)- ознака, що підстановка не працює.
Що робити: якщо нащадок не може виконати контракт, він не має бути нащадком. Розділити інтерфейси (ReadableRepository, WritableRepository - принцип розділення інтерфейсів), використати композицію або незмінні об'єкти (незмінний квадрат цілком може бути незмінним прямокутником).
PHP частково допомагає: перевизначений метод не може звузити типи параметрів чи розширити тип повернення (контраваріантність і коваріантність перевіряються). Але поведінковий контракт рушій не перевіряє - лише тести й увага.