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

Наследование – одна из самых противоречивых вещей в ООП. Чем больше мы узнаем о нем, тем больше подводных камней встречается. Мало того, что оно добавляет в код невероятное количество новых понятий и особенностей поведения, так оно еще имеет фундаментальные изъяны. И если с первым все более менее понятно. На протяжении всех предыдущих уроков мы только и занимались тем, что переосмысливали работу с классами. То со вторым нужно разобраться.

Ключевая проблема с иерархиями в том что наш мир не иерархичен. Любая классификация всегда опирается на конкретный признак, который интересует нас в конкретный момент времени. И эта же классификация становится бесполезной, если берется другой признак. Это хорошо видно в интернет-магазинах, у которых навороченные фильтры для выбора товара: группировка по производителю, по применимости, по безопасности для детей и так далее. В каждой конкретной ситуации будет своя структура.

Возьмем понятие User. Статьи по наследованию часто любят показывать иерархии пользователей вызывая у разработчиков уверенность что мир так и устроен. Давайте попробуем прикинуть, по каким признакам можно построить иерархию пользователей:

  • По полу (MaleUser, FemaleUser)
  • На основе аутентификации (User, Guest)
  • По роли (Admin, Member)
  • По типу должности (Marketer, SalesManager, Programmer, Tester, Player)
  • По принадлежности к какой-либо группе (UserFromRussia, UserWhoLikesSpartak)
  • По источнику (UserFromFacebook, UserFromGithub)
  • ...

Все это может и будет встречаться в рамках даже одной программы. В зависимости от того, какую задачу мы решаем, может понадобитсья разное представление. Наследование не дает такой свободы, наоборот, оно гвоздями приколачивает нас к конкретной структуре, которую уже не поменять. Единственным выходом в рамках этой парадигмы становится еще большее число наследований. В итоге у нас будет комбинация всех возможных поведений, которые встретятся в программе, а это иерархии с десятками и сотнями классов. Не забудьте что все это какими-то образом должно согласовываться с интерфейсами, которые тоже могут расширять друг друга.

Выходом могло бы быть множественное наследование, но как показала жизнь некоторых языков (c++), множественное наследование делает все еще сложнее. Поэтому от него отказались все кто только могли.

В конечном итоге, у разработчиков сформировалась общая позиция по отношению к наследованию, которая звучит так: Композиция вместо наследования. Если попробовать загуглить эту фразу, то поисковик покажет невероятное количество статей по этой теме. Этот подход мы уже изучали в курсе PHP полиморфизм. Он сводится к более грамотному разделению зон ответственности в приложении, делегированию функциональности другим объектам, нужным в конкретных ситуациях.

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

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

Яркий пример использования наследования не по назначению, это смешивание разных уровней абстракции. Выше был пример про пользователей разделяемых по типу хранилища SQLUser. Возникает вопрос, как пользователь, с точки зрения нашей предметной области, связан с техническими аспектами хранения этих пользователей? Никак не связан, такой код в принципе не должен существовать и наследование для него не предназначено.

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

Парадокс состоит в том, что фраза "composition over inheritance" (композиция вместо наследования), относится именно к такому использованию наследования. То есть проблема не в наследовании как таковом, а в том что оно оказалось удобным способом организации кода, для тех кто не очень хорошо знает как его организовывать, что такое барьеры абстракции и слои приложения.

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

Необходимость наследования классов возникает там, где классы связаны общим кодом (это не отношение подтипов). В такой ситуации нужна какая-то альтернатива наследованию. И здесь появляются варианты.

В самом простом случае, общий код не публичный. Тогда хватит обычной функции, которую эти классы будут использовать внутри себя. А если общий код был публичным? Большинство руководств рекомендуют создать соответствующий интерфейс и реализовать его в каждом из классов. Основной недостаток такого подхода – дублирование кода в каждом классе. То есть мы пришли к тому, от чего пытались уйти.

Решение этой проблемы известно довольно давно и называется миксины. Миксины – настоящая альтернатива правильному использованию наследования. С ними пропадает любая необходимость использовать наследование включая абстрактные классы. В PHP концепция миксинов нашла отражение в виде конструкции Trait. Трейтам посвящен следующий урок.

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

Хекслет

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