Принцип подстановки Барбары Лисков

Это гостевой выпуск Пятиминутки PHP — ведёт Кирилл Сулимовский.

Также порекомендую подписаться на телеграм канал Кирилла: https://t.me/beerphp

Принцип подстановки Барбары Лисков

❓Почему у многих возникают проблемы с этим принципом? Если взять не заумное , а более простое объяснение, то оно звучит так:

«Наследующий класс должен дополнять, а не замещать поведение базового класса».

Звучит логично и понятно, расходимся. но блин, как этого добиться? Почему-то многие просто пропускают мимо ушей следующие строки, которые как раз отлично объясняют что нужно делать.

Рассмотрим выражение «Предусловия не могут быть усилены в подклассе»

❗️Другими словами дочерние классы не должны создавать больше предусловий, чем это определено в базовом классе, для выполнения некоторого бизнесового поведения. Вот несложный пример.

❌ Добавление второго условия как раз является усилением. Так делать не надо!

Контравариантность также можно отнести к данному приниципу. Она касается параметров функции, которые может ожидать подкласс. Подкласс может увеличить свой диапазон параметров, но он должен принять все параметры, которые принимает родительский

• Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает родительский метод. 

• В этом примере дочерний класс может принимать более широкий спектр объектов, которые могут быть наследованы от Money, не только Dollars.

Таким образом, мы не добавляем дополнительных проверок, не делаем условия жестче и наш дочерний класс уже ведёт себя более предсказуемо. 

В следующих постах мы также рассмотрим постуловия и ковариантность ;)

«Постусловия не могут быть ослаблены в подклассе».

То есть подклассы должны выполнять все постусловия, которые определены в базовом классе. Постусловия проверяют состояние возвращаемого объекта на выходе из функции. Вот такой пример.

❌Условное выражение проверяющее результат является постусловием в базовом классе, а в наследнике его уже нет. Не делай так!

Сюда-же можно отнести и ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа (ШО?!), который возвращает родительский метод.

Короче, в данном примере, в методе render() дочернего класса, JpgImage объявлен типом возвращаемого значения, который в свою очередь является подтипом Image, который возвращает метод родительского класса Renderer

❗️Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. А усилили :)

❗️Все условия базового класса — также должны быть сохранены и в подклассе.

Инварианты — это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта. Например типы свойств базового класса не должны изменяться в дочернем.

class Wallet
{
    protected float $amount;
    // тип данного свойства не должен изменяться в подклассе
}

Здесь также стоит упомянуть исторические ограничения («правило истории»):

❗️Подкласс не должен создавать новых мутаторов свойств базового класса.

Если базовый класс не предусматривал методов для изменения определенных в нем свойств, подтип этого класса так же не должен создавать таких методов. Иными словами, неизменяемые данные базового класса не должны быть изменяемыми в подклассе

❌ С точки зрения класса Deposit поле не может быть меньше нуля. А вот производный класс VipDeposit, добавляет метод для изменения свойства account, поэтому инвариант класса Deposit нарушается. Такого поведения следует избегать. В таком случае стоит рассмотреть добавление мутатора в базовый класс.