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

И хотя довольно часто может казаться, что два класса делают одно и тоже, это не означает, что один из них может быть наследником другого. И «человек», и «утка» могут крякать, но это ведь не означает, что они родственники? Что же делать, когда ну уж совсем не хочется дублировать один и тот же код в разных частях программы?

В качестве примера плохого наследования рассмотрим такой вариант:

У нас есть класс Vehicle (средство передвижения на ДВС), которое умеет издавать звук («Whroom!»). Так же у нас есть класс ElectroVehicle (средство передвижения на электродвигателе), который также умеет издавать звук («Зуум!»). Еще у нас есть наследники этих классов: Car (автомобиль), ElectroCar (электромобиль) и Airplane (самолет). Car и ElectroCar кроме прочего умеют издавать предупреждающий сигнал («Beep!»), а Airplane умеет летать. Также у нас есть несколько реализаций этих классов: автомобили Ford и BMW, электромобиль Tesla и самолет Boing. С первого взгляда может показаться, что все вышло довольно-таки неплохо, но это не так:

Мы имеем дублирование кода в классах Car и ElectroCar (метод beep). Это произошло из-за того, что мы не смогли разместить метод beep в общем предке Vehicle, потому что не все Vehicle умеют издавать предупреждающий сигнал.
Классом ElectroVehicle мы переопределили, а не расширили метод do_whroom родителя. Тем самым мы нарушили принцип подстановки Барбары Лисков и в дальнейшем, когда в коде программы мы будем работать с экземпляром ElectroVehicle как с обычным Vehicle у нас будут проблемы. Например, наша программа будет ожидать появления строки «Whroom!» после вызова метода do_whroom, но сколько она не будет пытаться получить этот результат от Tesla — на выходе всегда будет «Zoom!»

Со временем, мы обнаружим, что электромобили — это не родственники автомобилей, но будет уже поздно. Чем больше мы будем писать кода в Vehicle, тем чаще нам будет необходимо переопределять его в ElectroVehicle.
К счастью, есть такая вещь как композиция. Композиция – это техника программирования при которой новые классы создаются путем помещения обобщенных функциональных модулей в объект-контейнер, который впоследствии использует и управляет этими модулями. Если при наследовании говорят, что наследник «является» родителем (Пользователь IS_A Администратор), то при композиции говорят, что один объект «владеет» другим (Пользователь HAS_A РольАдминистратора).

На Ruby простая композиция для описанного выше примера может быть реализована следующим образом:

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

Решение ее кроется в компромиссе между композицией и наследованием, рассмотрим еще один пример:

Классов стало больше, но в то же время ответственность каждого класса сокращена. Это действительно удобно и правильно с точки зрения кода – каждый класс отвечает только за свои методы, каждый из них определен в одном единственном месте, их легко поддерживать и развивать, зная, что пока они следуют определенному интерфейсу – ничего не поломается. В Ruby on Rails для класса ActiveRecord::Base есть даже специальный метод composed_of, который обеспечивает подобную реализацию:

Теперь объекты Car при извлечении из БД с помощью композиции получат свойство engine класса Engine, параметром power конструктора будет передано значение столбца engine_power_hp из БД. Кстати, имя класса rails определит по имени первого параметра метода composed_of. Это пример того, как в rails реализуется принцип CoC — Convention Over Configuration (соглашение важнее настроек). :engine — соответствует классу Engine, :wheel, например, будет соответствовать классу Wheel. Для переопределения этого правила есть специальный параметр class_name, но это уже совсем другая история.

Невозможно ответить, на вопрос: «Что лучше, наследование или композиция?», т.к. сама постановка такого вопроса не верна. Да, они служат одной и той же цели, но предназначены для использования в разных ситуациях. Просто заменить одно другим – это неправильно. Тут важен баланс, и, если посмотреть со стороны, этот баланс выглядит вполне естественным. Когда и что выбрать? С некоторыми упрощениями, можно утверждать следующее:

  • Если новый класс по смыслу является тем же, что и существующий, если он делает все тоже самое, но только лучше/быстрее/точнее и, если при этом в любой гипотетической ситуации экземпляр существующего класса легко и безболезненно заменяется экземпляром нового класса (принцип подстановки Барбары Лисков) – смело наследуемся.

  • Если же новый класс по смыслу лишь отдаленно соответствует уже существующему и в основном используется для других целей (пусть даже очень похожих), и уж тем более если часть реализации существующего класса теряет свой смысл или вообще желательно бы скрыть от чужих глаз – используем композицию.