Композиция по наследованию (или составной принцип повторного использования ) в объектно-ориентированном программировании (ООП) - это принцип, согласно которому классы должны достигать полиморфного поведения и повторное использование кода посредством их композиции (путем включения экземпляров других классов, реализующих желаемую функциональность), а не наследования от базового или родительского класса. Это часто упоминаемый принцип ООП, например, во влиятельной книге Шаблоны проектирования.
Реализация композиции вместо наследования обычно начинается с создание различных интерфейсов, представляющих поведение, которое должна демонстрировать система. Интерфейсы позволяют полиморфное поведение. Классы, реализующие идентифицированные интерфейсы, создаются и добавляются в классы бизнес-домена по мере необходимости. Таким образом, поведение системы реализуется без наследования.
Фактически, все классы бизнес-домена могут быть базовыми классами без какого-либо наследования. Альтернативная реализация поведения системы достигается путем предоставления другого класса, реализующего интерфейс желаемого поведения. Класс, содержащий ссылку на интерфейс, может поддерживать реализации интерфейса - выбор, который может быть отложен до времени выполнения.
Пример на C ++ следующий:
class Object {public: virtual void update () {// no- op} virtual void draw () {// no-op} virtual void collide (объектные объекты) {// no-op}}; class Visible: public Object {Модель * модель; public: virtual void draw () override {// код для рисования модели в позиции этого объекта}}; class Solid: public Object {public: virtual void collide (Object objects) override {// код для проверки и реагирования на столкновения с другими объектами}}; class Movable: public Object {public: virtual void update () override {// код для обновления положения этого объекта}};
Затем предположим, что у нас также есть эти конкретные классы:
Player
- это Solid
, Movable
и Visible
Cloud
- это Movable
и Visible
, но не Solid
Building
- это Solid
и Visible
, но не Movable
Trap
- это Solid
, но не Visible
ни Подвижный
Обратите внимание, что множественное наследование опасно, если не реализовано осторожно, поскольку оно может привести к проблеме ромба. Одним из решений, позволяющих избежать этого, является создание классов, таких как VisibleAndSolid
, VisibleAndMovable
, VisibleAndSolidAndMovable
и т. Д. Для каждой необходимой комбинации, хотя это приводит к большому количеству повторяющийся код. Помните, что C ++ решает проблему множественного наследования, разрешая виртуальное наследование.
Примеры C ++ в этом разделе демонстрируют принцип использования композиции и интерфейсов для повторного использования кода и полиморфизм. Поскольку в языке C ++ нет специального ключевого слова для объявления интерфейсов, в следующем примере C ++ используется «наследование от чисто абстрактного базового класса». Для большинства целей это функционально эквивалентно интерфейсам, предоставляемым на других языках, таких как Java и C #.
Представьте абстрактный класс с именем VisibilityDelegate
с подклассами NotVisible
и Visible
, который обеспечивает средства рисования объекта:
класс VisibilityDelegate {общедоступные: виртуальная пустота draw () = 0; }; class NotVisible: public VisibilityDelegate {public: virtual void draw () override {// no-op}}; class Visible: public VisibilityDelegate {public: virtual void draw () override {// код для рисования модели в позиции этого объекта}};
Представьте абстрактный класс с именем UpdateDelegate
с подклассами NotMovable
и Movable
, который предоставляет средства перемещения объекта:
class UpdateDelegate { общедоступные: виртуальное обновление void () = 0; }; class NotMovable: public UpdateDelegate {public: virtual void update () override {// no-op}}; class Movable: public UpdateDelegate {public: virtual void update () override {// код для обновления положения этого объекта}};
Представьте абстрактный класс с именем CollisionDelegate
с подклассами NotSolid
и Solid
, который предоставляет средства столкновения с объектом:
класс CollisionDelegate {общедоступные: виртуальная пустота столкновения (объекты объекта) = 0; }; class NotSolid: public CollisionDelegate {public: virtual void collide (Object objects) override {// no-op}}; class Solid: public CollisionDelegate {public: virtual void collide (Object objects) override {// код для проверки и реакции на столкновения с другими объектами}};
Наконец, представьте класс с именем Object
с членами для управления его видимостью (используя VisibilityDelegate
), подвижностью (используя UpdateDelegate
) и твердостью ( с использованием CollisionDelegate
). У этого класса есть методы, которые делегируют его членам, например update ()
просто вызывает метод класса UpdateDelegate
:
Object {VisibilityDelegate * _v; UpdateDelegate * _u; CollisionDelegate * _c; общедоступные: Object (VisibilityDelegate * v, UpdateDelegate * u, CollisionDelegate * c): _v (v), _u (u), _c (c) {} void update () {_u->update (); } void draw () {_v->draw (); } void collide (объекты-объекты) {_c->collide (объекты); }};
Тогда конкретные классы будут выглядеть так:
class Player: public Object {public: Player (): Object (new Visible (), new Movable (), new Solid ()) {} //... }; class Smoke: public Object {public: Smoke (): Object (new Visible (), new Movable (), new NotSolid ()) {} //...};
Преимущество композиции перед наследованием - это принцип проектирования, который придает дизайну большую гибкость. Более естественно строить классы бизнес-домена из различных компонентов, чем пытаться найти между ними общие черты и создавать семейное древо. Например, педаль акселератора и рулевое колесо имеют очень мало общих черт, но оба являются жизненно важными компонентами в автомобиле. Легко определить, что они могут делать и как их можно использовать в интересах автомобиля. Композиция также обеспечивает более стабильную сферу бизнеса в долгосрочной перспективе, поскольку она менее подвержена причудам членов семьи. Другими словами, лучше скомпоновать то, что объект может делать (HAS-A ), чем расширять то, что он есть (IS-A ).
Первоначальный дизайн упрощается путем определения поведения системных объектов в отдельные интерфейсы вместо создания иерархических отношений для распределения поведения между классами бизнес-домена посредством наследования. Такой подход более легко учитывает будущие изменения требований, которые в противном случае потребовали бы полной реструктуризации классов бизнес-домена в модели наследования. Кроме того, он часто позволяет избежать проблем связаны с относительно небольшими изменениями в модели на основе наследования, которая включает несколько поколений классов.
Некоторые языки, особенно Go, используют исключительно композицию типов.
Одним из распространенных недостатков использования композиции вместо наследования является то, что методы, предоставляемые отдельными компонентами, могут быть реализованы в производном типе, даже если они являются только методами пересылки (это верно в большинстве п языки программирования, но не все; см. Как избежать недостатков.) Напротив, наследование не требует, чтобы все методы базового класса были повторно реализованы в производном классе. Скорее, производный класс должен только реализовать (переопределить) методы, поведение которых отличается от поведения методов базового класса. Это может потребовать значительно меньших усилий по программированию, если базовый класс содержит много методов, обеспечивающих поведение по умолчанию, и только некоторые из них необходимо переопределить в производном классе.
Например, в приведенном ниже коде C # переменные и методы базового класса Employee
наследуются производными от HourlyEmployee
и SalariedEmployee
подклассы. Только метод Pay ()
должен быть реализован (специализирован) каждым производным подклассом. Другие методы реализуются самим базовым классом и являются общими для всех его производных подклассов; их не нужно повторно реализовывать (переопределять) или даже упоминать в определениях подклассов.
// Открытый абстрактный класс базового класса Employee {// Свойства защищенной строки Name {get; задавать; } защищенный int ID {получить; задавать; } защищенная десятичная ставка PayRate {получить; задавать; } protected int HoursWorked {получить; } // Получить оплату за текущий платежный период public abstract decimal Pay (); } // Производный подкласс public class HourlyEmployee: Employee {// Получить оплату за текущий платежный период public override decimal Pay () {// Отработанное время указано в часах return HoursWorked * PayRate; }} // Производный подкласс public class SalariedEmployee: Employee {// Получить оплату за текущий платежный период public override decimal Pay () {// Ставка оплаты - это годовая зарплата вместо почасовой ставки return HoursWorked * PayRate / 2087; }}
Этого недостатка можно избежать, используя traits, mixins, (type) embedding или протокол расширений.
Некоторые языки предоставляют специальные средства для смягчения этого:
@Delegate
в поле вместо копирования и поддержки имена и типы всех методов из делегированного поля. Java 8 позволяет использовать методы по умолчанию в интерфейсе, аналогично C # и т. Д.
ключевое слово для облегчения перенаправления методов.Исследование 93 программ Java с открытым исходным кодом (различного размера) в 2013 году показало, что:
Хотя нет [sic] возможности заменить наследование составом (...), возможность значительна (в среднем 2% использования [наследования] представляют собой только внутреннее повторное использование, а следующие 22% - только внешнее или внутреннее повторное использование). Наши результаты показывают, что нет необходимости беспокоиться о злоупотреблении наследованием (по крайней мере, в программном обеспечении Java с открытым исходным кодом), но они подчеркивают вопрос, касающийся использования композиции по сравнению с наследованием. Если использование наследования связано со значительными затратами, когда можно использовать композицию, наши результаты предполагают, что есть некоторые причины для беспокойства.
— Tempero et al., «Что программисты делают с наследованием в Java»