HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-26
1 <p>Переопределение методов на техническом уровне ничем не ограничено. Класс наследник может изменить поведение любого метода настолько, насколько это вообще возможно. С одной стороны, может показаться что это здорово, так как открывается большая свобода действий, но с другой, некоторые изменения могут повлечь за собой серьёзные архитектурные проблемы. Самая главная из них - сломанный полиморфизм.</p>
1 <p>Переопределение методов на техническом уровне ничем не ограничено. Класс наследник может изменить поведение любого метода настолько, насколько это вообще возможно. С одной стороны, может показаться что это здорово, так как открывается большая свобода действий, но с другой, некоторые изменения могут повлечь за собой серьёзные архитектурные проблемы. Самая главная из них - сломанный полиморфизм.</p>
2 <p>Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения), базирующийся на<a>PSR3</a>.</p>
2 <p>Рассмотрим пример. Допустим, мы решили написать свой собственный логгер (объект, который записывает в журнал произвольные сообщения), базирующийся на<a>PSR3</a>.</p>
3 <p>Логгер позволяет записывать сообщения с разным уровнем важности, начиная от<em>debug</em>и до<em>emergency</em>. Сигнатура метода log устроена таким образом, что первым параметром всегда передаётся уровень сообщения, а вторым - сообщение. Само сообщение - это строка произвольного формата.</p>
3 <p>Логгер позволяет записывать сообщения с разным уровнем важности, начиная от<em>debug</em>и до<em>emergency</em>. Сигнатура метода log устроена таким образом, что первым параметром всегда передаётся уровень сообщения, а вторым - сообщение. Само сообщение - это строка произвольного формата.</p>
4 <p>Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим подтип<em>MyLoggerInterface</em>с переопределённой сигнатурой метода<em>log</em>, а затем реализуем его в классе<em>MyLogger</em>.</p>
4 <p>Предположим, что нам это не понравилось, и мы решили изменить сигнатуру так, чтобы уровень передавался вторым параметром. Это позволит задать нам значение по умолчанию для того уровня, который чаще всего встречается в приложении. Для этого создадим подтип<em>MyLoggerInterface</em>с переопределённой сигнатурой метода<em>log</em>, а затем реализуем его в классе<em>MyLogger</em>.</p>
5 <p>Что не так с этим кодом? Если вы прошли курс по полиморфизму, то ответ должен быть очевиден. Так как наш класс реализует интерфейс<em>MyLoggerInterface</em>, то он реализует и<em>LoggerInterface</em>. Это значит, что в любом месте где требуется последний, мы можем передать объект класса<em>MyLogger</em>:</p>
5 <p>Что не так с этим кодом? Если вы прошли курс по полиморфизму, то ответ должен быть очевиден. Так как наш класс реализует интерфейс<em>MyLoggerInterface</em>, то он реализует и<em>LoggerInterface</em>. Это значит, что в любом месте где требуется последний, мы можем передать объект класса<em>MyLogger</em>:</p>
6 <p>Этот код завершится с ошибкой, так как объект $database будет использовать логгер в соответствии с требованиями<em>LoggerInterface</em>, что противоречит интерфейсу<em>MyLoggerInterface</em>. Фактически, это означает, что структура типов построена неверно, даже несмотря на то, что PHP его пропустил.</p>
6 <p>Этот код завершится с ошибкой, так как объект $database будет использовать логгер в соответствии с требованиями<em>LoggerInterface</em>, что противоречит интерфейсу<em>MyLoggerInterface</em>. Фактически, это означает, что структура типов построена неверно, даже несмотря на то, что PHP его пропустил.</p>
7 <p>В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle - LSP), следование которому позволяет правильно строить иерархии типов:</p>
7 <p>В 1987 году Барбара Лисков сформулировала принцип подстановки (Liskov Substitution Principle - LSP), следование которому позволяет правильно строить иерархии типов:</p>
8 <blockquote><p>Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.</p>
8 <blockquote><p>Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.</p>
9 </blockquote><p>Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:</p>
9 </blockquote><p>Звучит математично. Многие разработчики пытались переформулировать это правило так, чтобы оно было интуитивно понятным. Самая простая формулировка звучит так:</p>
10 <blockquote><p>Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.</p>
10 <blockquote><p>Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.</p>
11 </blockquote><p>То что нужно. В примере выше функция setLogger($logger) ожидает на вход тип LoggerInterface, а мы передали ей подтип MyLoggerInterface. Согласно принципу, код должен продолжать работать как ни в чём не бывало, но этого не происходит из-за нарушения интерфейса.</p>
11 </blockquote><p>То что нужно. В примере выше функция setLogger($logger) ожидает на вход тип LoggerInterface, а мы передали ей подтип MyLoggerInterface. Согласно принципу, код должен продолжать работать как ни в чём не бывало, но этого не происходит из-за нарушения интерфейса.</p>
12 <p><em>Для любознательных. Этот принцип любят показывать на иерархиях наследования классов, но как вы видите из текста выше, этот принцип относится к интерфейсам, а не классам. Иерархии классов не обязаны следовать ему, хотя было бы неплохо.</em></p>
12 <p><em>Для любознательных. Этот принцип любят показывать на иерархиях наследования классов, но как вы видите из текста выше, этот принцип относится к интерфейсам, а не классам. Иерархии классов не обязаны следовать ему, хотя было бы неплохо.</em></p>
13 <p><em>Для ещё более любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.</em></p>
13 <p><em>Для ещё более любознательных. Почему вообще понадобился этот принцип? Почему бы не поручить эту работу языку? К сожалению, технически невозможно убедиться в соблюдении принципа Лисков. Поэтому его выполнение ложится на плечи разработчиков.</em></p>
14 <h2>Правила проектирования иерархий типов</h2>
14 <h2>Правила проектирования иерархий типов</h2>
15 <p>Существует несколько правил, которые надо учитывать при работе с типами:</p>
15 <p>Существует несколько правил, которые надо учитывать при работе с типами:</p>
16 <ul><li>Предусловия не могут быть усилены в подклассе</li>
16 <ul><li>Предусловия не могут быть усилены в подклассе</li>
17 <li>Постусловия не могут быть ослаблены в подклассе</li>
17 <li>Постусловия не могут быть ослаблены в подклассе</li>
18 <li>Исторические ограничения</li>
18 <li>Исторические ограничения</li>
19 </ul><p>Предусловия - это ограничения на входные данные, а постусловия - на выходные. Причём в силу ограничений систем типов, многие из таких условий невозможно описать на уровне интерфейсов. Их либо придётся описывать просто текстом, как это сделано в документации PSR, либо добавлять проверки в код (<a>проектирование по контракту</a>).</p>
19 </ul><p>Предусловия - это ограничения на входные данные, а постусловия - на выходные. Причём в силу ограничений систем типов, многие из таких условий невозможно описать на уровне интерфейсов. Их либо придётся описывать просто текстом, как это сделано в документации PSR, либо добавлять проверки в код (<a>проектирование по контракту</a>).</p>
20 <p>Например, в нашем логгере (в интерфейсе<em>LoggerInterface</em>) предусловием является то, что метод log первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс, который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жёстче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причём не важно, приведёт это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.</p>
20 <p>Например, в нашем логгере (в интерфейсе<em>LoggerInterface</em>) предусловием является то, что метод log первым параметром принимает один из 8 уровней сообщений. Принцип Лисков утверждает, что мы не можем создать класс, реализующий этот интерфейс, который может обрабатывать меньшее число уровней. Это и называется усилением предусловий, то есть требования становятся жёстче. Вместо 8 уровней, например 5. Попытка использовать объект такого класса, закончится ошибкой, когда какая-то из систем попробует передать ему уровень, который не поддерживается. Причём не важно, приведёт это к ошибке (исключению) или логгер молча проглотит это сообщение не записав его в журнал. Главное, что поведение стало отличаться.</p>
21 <p><em>Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.</em></p>
21 <p><em>Встречаются ситуации, когда разработчики не видя причину такого поведения, начинают лечить следствия. В местах, где используются подобные объекты, добавляются проверки на типы. А это убивает полиморфизм.</em></p>
22 <p>С постусловиями ситуация аналогичная, но наоборот. Допустимо, если метод возвращает урезанный набор значений, так как этот набор все равно укладывается в требования интерфейса. А вот расширять возврат нельзя, так как появляются значения, которые не были предусмотрены интерфейсом. Это относится и к исключениям.</p>
22 <p>С постусловиями ситуация аналогичная, но наоборот. Допустимо, если метод возвращает урезанный набор значений, так как этот набор все равно укладывается в требования интерфейса. А вот расширять возврат нельзя, так как появляются значения, которые не были предусмотрены интерфейсом. Это относится и к исключениям.</p>
23 <p>И последнее, исторические ограничения. Подтипы не могут добавлять новые методы для изменения (мутации) данных базового типа. Способы изменения свойств, определённых в базовом типе определяются этим типом.</p>
23 <p>И последнее, исторические ограничения. Подтипы не могут добавлять новые методы для изменения (мутации) данных базового типа. Способы изменения свойств, определённых в базовом типе определяются этим типом.</p>