Принцип подстановки Барбары Лисков
Почему у многих возникают проблемы с этим принципом? Если взять не заумное , а более простое объяснение, то оно звучит так: (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
Нет Ответов