Главная

Используйте Композицию вместо Наследования.

Наследование может значительно усложнить код, в то время как Композиция сделает его гибче.

Поделиться:
 

31+

Наследование — это одна из трех основных концепций ООП. Вероятно, наследование было вторым принципом, который вы поняли после инкапсуляции. Идея довольно простая — вы можете создать дочерний класс, который наследует свойства и возможности родительского класса. Не так уж и сложно, правда?

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

Наследование, которое вышло из-под контроля

Представим, что у вас есть пиццерия. Вы хотите написать систему для создания пиццы. Итак, начнем с базового класса:

Окей, теперь у нас есть абстрактный класс и мы будем от него наследоваться. Каждая уважающая себя пицца имеет томатный соус, поэтому мы создадим еще один класс, который наследуется от Pizza.

Также нам понадобится сыр…

Хорошо, но клиенты захотят больше ингредиентов. Начнем с пепперони и грибов:

И, да, стоит учесть, что некоторые клиенты могут захотеть и то и другое, поэтому вам понадобится:

Теперь добавим в колбасу лук, анчоусы и маслины. Хм, достаточно сложная задача, не так ли? Используя Наследование, у вас быстро создается достаточно глубокая и широкая модель. Свойства базового класса не будут охватывать все возможные ситуации, которые возникают при добавлении большего количества ингредиентов. Общий дизайн быстро становится громоздким и негибким. Если вы захотите добавить новый элемент — вам придется написать очень много дополнительного кода.

Проблемы

Наследование — это круто, но у него есть свои недостатки:
1. Оно способствует созданию громоздких подклассов. Ваш код может превратиться в кашу из множества подклассов, которые, к тому же, не могут удовлетворить все ваши потребности.
2. Нельзя сказать с полной точностью, какими свойствами должен обладать суперкласс. Возможно, он находится в модуле, который вне вашего контроля, и имеет много зависимостей, что может привести к непредсказуемым результатам.
3. Иерархию классов очень трудно изменить после их развертывания. Возможно, у вас есть пицца, которую никто никогда не заказывает — убрать ее будет проблематично. Создание же новой, особенной пиццы может стать неподъёмной задачей. Изменяя суперкласс, вы рискуете нарушить работу подклассов. Но есть и лучший способ. Что насчет Композиции?

Это прекрасный пример того, почему вы должны предпочесть композицию наследованию. Композиция — более гибкий инструмент, позволяющий использовать несколько типов сыров, видов соусов и практически неограниченный набор ингредиентов. Приведенный выше пример может легко иметь закрытые поля, открытые свойства и конструктор, позволяющий создавать все это за один раз. Это может выглядеть так:

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

Интерфейс композиции

Другой способ взглянуть на композицию через призму наследования — интерфейсы. Если вы знакомы с принципами SOLID, вы должны знать, что принцип сегрегации интерфейсов гласит: интерфейсы должны быть небольшими и простыми. Интерфейсы можно успешно использовать при создании классов. Это приведет к увеличению гибкости.

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

Например, возьмите классический пример автомобиля. Принято демонстрировать идею наследования с помощью транспортных средств, добавляя к ним колеса, методы управления и т.п. Но в какой-то момент надо создать уже лодку и вы в недоумении… Композиция интерфейсов допускает подобные вещи.
Учтите следующее:

Здесь у нас есть коллекция интерфейсов, которые можно объединить и составить любой автомобиль, даже лодку. Обратите внимание, что Boat и Car не отличаются друг от друга в декларировании, за исключением того, что Car имеет колеса и может тормозить.
Различных доступных интерфейсов достаточно, чтобы определить функциональные возможности практически любого транспортного средства, и если их нет, достаточно просто добавить еще один.
В приведенном выше примере можно добавить ILight и ITrailerHitch в качестве интерфейсов, которые будут использоваться транспортными средствами. Оба интерфейса могут быть добавлены с относительной легкостью и отсутствием сложностей с существующими классами.

Заключение

Таким образом, Композиция предпочтительнее по ряду причин:

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

Предпочитайте Композицию Наследованию, и у вас будет более гибкий, расширяемый и тестируемый код.

Спасибо!

Онлайн-курс по Java-разработке от Skillbox

31+