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