Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

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

Рассмотрим пример. Допустим мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения), базирующийся на PSR3.

<?php

// Определение
class MyLogger implements LoggerInterface {
   // код
}

// Использование

$logger = new MyLogger();
$logger->log('debug', 'Doing work');
$logger->log('info', 'Usefull for debugging');

Логгер позволяет записывать сообщения с разным уровнем важности, начиная от debug и до emergency. Сигнатура метода log устроена таким образом, что первым параметром всегда передается уровень сообщения, а сообщение вторым. Само сообщение это строка произвольного формата.

Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам дефолтное значение для того уровня, который чаще всего встречается в приложении. Для этого создадим подтип MyLoggerInterface, с переопределенной сигнатурой метода log, а затем реализуем его в классе MyLogger

<?php

// PHP позволяет так сделать

interface MyLoggerInterface extends LoggerInterface
{
    // В LoggerInterface: public function log($level, $message, array $context = []);
    public function log($message, $level = 'info', array $context = []);
}

class MyLogger implements MyLoggerInterface {
   // Тут реализуем новую сигнатуру log
}

// Использование

$logger->log('Doing work'); // По умолчанию debug
$logger->log('Usefull for debugging', 'info');

Что не так с этим кодом? Если вы прошли курс по полиморфизму, то ответ должен быть очевиден. Так как наш класс реализует интерфейс MyLoggerInterface, то он реализует и LoggerInterface. Это значит, что в любом месте где требуется последний, мы можем передать объект класса MyLogger:

<?php
// Предположим что какой-то компонент системы хочет работать с логгером соответствующим стандарту PSR3
$logger = new MyLogger();
$database->setLogger($logger);

$database->doSomething();
// Внутри вызывается логгер
// $logger->log('info', 'boom!');

Этот код завершится с ошибкой, так как объект $database будет использовать логгер в соответствии с требованиями LoggerInterface, что противоречит интерфейсу MyLoggerInterface. Фактически, это означает что структура типов построена неверно, даже не смотря на то, что PHP его пропустил.

В 1987 году, Барбара Лисков, сформулировала принцип подстановки (Liskov Substitution Principle – LSP), следование которому позволяет правильно строить иерархии типов:

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:

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

То что нужно. В примере выше функция setLogger($logger) ожидает на вход тип LoggerInterface, а мы передали ей подтип MyLoggerInterface. Согласно принципу, код должен продолжать работать как ни в чем не бывало, но этого не происходит из-за нарушения интерфейса.

Для любознательных. Этот принцип любят показывать на иерархиях наследования классов, но как вы видите из текста выше, этот принцип относится к интерфейсам, а не классам. Иерархии классов не обязаны следовать ему, хотя было бы неплохо.

Для еще более любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков

Правила проектирования иерархий типов

Существует несколько правил, которые надо учитывать при работе с типами:

  • Предусловия не могут быть усилены в подклассе
  • Постусловия не могут быть ослаблены в подклассе
  • Исторические ограничения

Предусловия – это ограничения на входные данные, а постусловия – на выходные. Причем в силу ограничений систем типов, многие из таких условий невозможно описать на уровне интерфейсов. Их либо придется описывать просто текстом, как это сделано в документации PSR, либо добавлять проверки в код (проектирование по контракту).

Например в нашем логгере (в интерфейсе LoggerInterface) предусловием является то, что метод log первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утвержает, что мы не можем создать класс реализующий этот интерфейс, который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жестче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причем не важно, приведет это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное что поведение стало отличаться.

Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах где используются подобные объекты добавляются проверки на типы. А это убивает полиморфизм

С постусловиями ситуация аналогичная, но наоборот. Допустимо если метод возвращает урезанный набор значений, так как этот набор все равно укладывается в требования интерфейса. А вот расширять возврат нельзя, так как появляются значения, которые не были предусмотрены интерфейсом. Это относится и к исключениям.

И последнее, исторические ограничения. Подтипы не могут добавлять новые методы для изменения (мутации) данных базового типа. Способы изменения свойств определенных в базовом типе определяются этим типом.


Дополнительные материалы

  1. Circle-ellipse problem
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →