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