Принцип подстановки Барбары Лисков
Почему у многих возникают проблемы с этим принципом? Если взять не заумное , а более простое объяснение, то оно звучит так: (1)
«Наследующий класс должен дополнять, а не замещать поведение базового класса».
Звучит логично и понятно, расходимся. но блин, как этого добиться? Почему-то многие просто пропускают мимо ушей следующие строки, которые как раз отлично объясняют что нужно делать.
Рассмотрим выражение «Предусловия не могут быть усилены в подклассе»
Другими словами дочерние классы не должны создавать больше предусловий, чем это определено в базовом классе, для выполнения некоторого бизнесового поведения. Вот несложный пример:
<?php
class Customer
{
protected float $account = 0;
public function putMoneyIntoAccount(int|float $sum): void
{
if ($sum < 1) {
throw new Exception('Вы не можете положить на счёт меньше 1$');
}
$this->account += $sum;
}
}
class MicroCustomer extends Customer
{
public function putMoneyIntoAccount(int|float $sum): void
{
if ($sum < 1) {
throw new Exception('Вы не можете положить на счёт меньше 1$');
}
// Усиление предусловий
if ($sum > 100) {
throw new Exception('Вы не можете положить на больше 100$');
}
$this->account += $sum;
}
}
Добавление второго условия как раз является усилением. Так делать не надо!
Контравариантность также можно отнести к данному приниципу. Она касается параметров функции, которые может ожидать подкласс. Подкласс может увеличить свой диапазон параметров, но он должен принять все параметры, которые принимает родительский.
- Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает родительский метод.
<?php
class Foo
{
public function process(int|float $value)
{
// some code
}
}
class Bar extends Foo
{
public function process(int|float|string $value)
{
// some code
}
}
- В этом примере дочерний класс может принимать более широкий спектр объектов, которые могут быть наследованы от Money, не только Dollars.
<?php
class Money {}
class Dollars extends Money {}
class Customer
{
protected Money $account;
public function putMoneyIntoAccount(Dollars $sum): void
{
$this->account = $sum;
}
}
class VIPCustomer extends Customer
{
public function putMoneyIntoAccount(Money $sum): void
{
$this->account = $sum;
}
}
Таким образом, мы не добавляем дополнительных проверок, не делаем условия жестче и наш дочерний класс уже ведёт себя более предсказуемо.
В следующих постах мы также рассмотрим постуловия и ковариантность 
«Постусловия не могут быть ослаблены в подклассе».
То есть подклассы должны выполнять все постусловия, которые определены в базовом классе. Постусловия проверяют состояние возвращаемого объекта на выходе из функции. Вот такой пример.
<?php
class Customer
{
protected Dollars $account;
public function chargeMoney(Dollars $sum): float
{
$result = $this->account - $sum->getAmount();
if ($result < 0) { // Постусловие
throw new Exception();
}
return $result;
}
}
class VIPCustomer extends Customer
{
public function chargeMoney(Dollars $sum): float
{
$result = $this->account - $sum->getAmount();
if ($sum < 1000) { // Добавлено новое поведение
$result -= 5;
}
// Пропущено постусловие базового класса
return $result;
}
}
Условное выражение проверяющее результат является постусловием в базовом классе, а в наследнике его уже нет. Не делай так!
Сюда-же можно отнести и ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа, который возвращает родительский метод.
Короче, в данном примере:
<?php
class Image {}
class JpgImage extends Image {}
class Renderer
{
public function render(): Image
{
}
}
class PhotoRenderer extends Renderer
{
public function render(): JpgImage
{
}
}
в методе render() дочернего класса, JpgImage объявлен типом возвращаемого значения, который в свою очередь является подтипом Image, который возвращает метод родительского класса Renderer.
Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. А усилили 
Все условия базового класса — также должны быть сохранены и в подклассе.
Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.
Например типы свойств базового класса не должны изменяться в дочернем.
class Wallet
{
protected float $amount;
// тип данного свойства не должен изменяться в подклассе
}
Здесь также стоит упомянуть исторические ограничения («правило истории»):
(!) Подкласс не должен создавать новых мутаторов свойств базового класса.
Если базовый класс не предусматривал методов для изменения определенных в нем свойств, подтип этого класса так же не должен создавать таких методов. Иными словами, неизменяемые данные базового класса не должны быть изменяемыми в подклассе.
(-) С точки зрения класса Deposit поле не может быть меньше нуля. А вот производный класс VipDeposit, добавляет метод для изменения свойства account, поэтому инвариант класса Deposit нарушается. Такого поведения следует избегать. В таком случае стоит рассмотреть добавление мутатора в базовый класс.
23.04.2021 | Автор Petr Myazin
Источники:
(1) https://5minphp.ru/episode88/
(2) Телеграм канал спикера: https://t.me/beerphp


Нет Ответов