Наследование – один из самых противоречивых механизмов ООП. Чем больше мы узнаем о нём, тем больше подводных камней встречается. Мало того, что оно добавляет в код невероятное количество новых понятий и особенностей поведения, так оно ещё имеет фундаментальные изъяны. И если с первым всё более менее понятно — на протяжении всех предыдущих уроков мы только и занимались тем, что переосмысливали работу с классами, то со вторым нужно разобраться.
Ключевая проблема с иерархиями в том, что наш мир не иерархичен. Любая классификация всегда опирается на конкретный признак, который интересует нас в конкретный момент времени. И эта же классификация становится бесполезной, если берётся другой признак. Это хорошо видно в интернет-магазинах, у которых навороченные фильтры для выбора товара: группировка по производителю, по применимости, по безопасности для детей и так далее. В каждой конкретной ситуации будет своя структура.
Возьмём понятие User. Статьи по наследованию часто любят показывать иерархии пользователей создавая у разработчиков уверенность, что мир так и устроен. Давайте попробуем прикинуть по каким признакам можно построить иерархию пользователей:
- По полу (MaleUser, FemaleUser)
- На основе аутентификации (User, Guest)
- По роли (Admin, Member)
- По типу должности (Marketer, SalesManager, Programmer, Tester, Player)
- По принадлежности к какой-либо группе (UserFromRussia, UserWhoLikesSpartak)
- По источнику (UserFromFacebook, UserFromGithub)
- По типу хранилища (SQLUser, LocalStorageUser)
- ...
Всё это может и будет встречаться в рамках даже одной программы. В зависимости от того, какую задачу мы решаем, может понадобиться разное представление. Наследование не даёт такой свободы, наоборот, оно гвоздями приколачивает нас к конкретной структуре, которую уже не поменять. Единственным выходом в рамках этой парадигмы становится ещё большее число наследований. В итоге у нас будет комбинация всех возможных поведений, которые встретятся в программе, а это иерархии с десятками и сотнями классов. Не забудьте, что всё это каким-то образом должно согласовываться с интерфейсами, которые тоже могут расширять друг друга.
Выходом могло бы быть множественное наследование, но множественное наследование делает все ещё сложнее, как показывает опыт (C++) и других языков. Поэтому от него отказались все, кто только могли.
В конечном итоге, у разработчиков сформировалась общая позиция по отношению к наследованию, которая звучит так: композиция вместо наследования. Если попробовать загуглить эту фразу, то поисковик покажет невероятное количество статей по этой теме. Этот подход мы уже изучали в курсе "PHP: Полиморфизм". Он сводится к более грамотному разделению зон ответственности в приложении, делегированию функциональности другим объектам, нужным в конкретных ситуациях.
С этого момента начинаются сложности. В большинстве статей, посвящённых этому вопросу, приводятся либо ошибочные, либо слишком искусственные примеры, которые не дают особого понимания. Для начала, отделим две разные причины использования наследования. Одна из них связана с прямым назначением наследования, другая вытекает из неверного понимания принципов организации кода.
Использование не по назначению
Яркий пример использования наследования не по назначению, это смешивание разных уровней абстракции. Выше был пример про пользователей разделяемых по типу хранилища — SQLUser. Возникает вопрос: как пользователь, с точки зрения нашей предметной области, связан с техническими аспектами хранения этих пользователей? Никак не связан, такой код в принципе не должен существовать и наследование для него не предназначено.
Такой код появляется не от незнания ООП, а от непонимания общих принципов организации кода, построения абстракций. Эта тема не является специфичной для ООП, но ООП делает её сложнее из-за большого числа новых сущностей, которое оно вводит. Обязательно прочитайте эту статью, которая проливает свет на архитектуру.
Парадокс состоит в том, что фраза "composition over inheritance" (композиция вместо наследования) относится именно к такому использованию наследования. То есть проблема не в наследовании как таковом, а в том, что оно оказалось удобным способом организации кода для тех, кто не очень хорошо знает, как его организовывать, что такое барьеры абстракции и слои приложения.
Использование по назначению
Необходимость наследования классов возникает там, где классы связаны общим кодом (это не отношение подтипов). В такой ситуации нужна какая-то альтернатива наследованию. И здесь появляются варианты.
В самом простом случае, общий код не публичный. Тогда хватит обычной функции, которую эти классы будут использовать внутри себя. А если общий код был публичным? Большинство руководств рекомендуют создать соответствующий интерфейс и реализовать его в каждом из классов. Основной недостаток такого подхода – дублирование кода в каждом классе. То есть мы пришли к тому, от чего пытались уйти.
Решение этой проблемы известно довольно давно и называется миксины. Миксины – настоящая альтернатива правильному использованию наследования. С ними пропадает любая необходимость использовать наследование, включая абстрактные классы. В PHP концепция миксинов нашла отражение в виде конструкции Trait. Трейтам посвящён следующий урок.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.