Композиция вместо наследования

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

Композиция по наследованию (или составной принцип повторного использования ) в объектно-ориентированном программировании (ООП) - это принцип, согласно которому классы должны достигать полиморфного поведения и повторное использование кода посредством их композиции (путем включения экземпляров других классов, реализующих желаемую функциональность), а не наследования от базового или родительского класса. Это часто упоминаемый принцип ООП, например, во влиятельной книге Шаблоны проектирования.

Содержание
  • 1 Основы
  • 2 Пример
    • 2.1 Наследование
    • 2.2 Состав и интерфейсы
  • 3 Преимущества
  • 4 Недостатки
    • 4.1 Избегание недостатков
  • 5 Эмпирические исследования
  • 6 См. Также
  • 7 Ссылки
Основы

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

Фактически, все классы бизнес-домена могут быть базовыми классами без какого-либо наследования. Альтернативная реализация поведения системы достигается путем предоставления другого класса, реализующего интерфейс желаемого поведения. Класс, содержащий ссылку на интерфейс, может поддерживать реализации интерфейса - выбор, который может быть отложен до времени выполнения.

Пример

Наследование

Пример на 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 {// код для обновления положения этого объекта}};

Затем предположим, что у нас также есть эти конкретные классы:

  • class Player- это Solid, Movableи Visible
  • class Cloud- это Movableи Visible, но не Solid
  • class Building- это Solidи Visible, но не Movable
  • class 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 или протокол расширений.

Некоторые языки предоставляют специальные средства для смягчения этого:

  • C# предоставляет методы интерфейса по умолчанию, начиная с версии 8.0, которые позволяют определять тело элемента интерфейса.
  • D предоставляет явное объявление «псевдоним this» внутри типа, которое может пересылать ему каждый метод и член другого содержащегося типа. Встраивание типа
  • Go устраняет необходимость в методах пересылки.
  • Java предоставляет Project Lombok, который позволяет реализовать делегирование с использованием единственной аннотации @Delegateв поле вместо копирования и поддержки имена и типы всех методов из делегированного поля. Java 8 позволяет использовать методы по умолчанию в интерфейсе, аналогично C # и т. Д.
  • Макросы Julia могут использоваться для генерации методов пересылки. Существует несколько реализаций, таких как Lazy.jl и TypedDelegation.jl.
  • Kotlin включает шаблон делегирования в синтаксис языка.
  • Raku предоставляет дескрипторы ключевое слово для облегчения перенаправления методов.
  • Rust предоставляет черты с реализациями по умолчанию.
  • Расширения Swift могут использоваться для определения реализации протокола по умолчанию в самом протоколе, а не в реализация индивидуального типа.
Эмпирические исследования

Исследование 93 программ Java с открытым исходным кодом (различного размера) в 2013 году показало, что:

Хотя нет [sic] возможности заменить наследование составом (...), возможность значительна (в среднем 2% использования [наследования] представляют собой только внутреннее повторное использование, а следующие 22% - только внешнее или внутреннее повторное использование). Наши результаты показывают, что нет необходимости беспокоиться о злоупотреблении наследованием (по крайней мере, в программном обеспечении Java с открытым исходным кодом), но они подчеркивают вопрос, касающийся использования композиции по сравнению с наследованием. Если использование наследования связано со значительными затратами, когда можно использовать композицию, наши результаты предполагают, что есть некоторые причины для беспокойства.

— Tempero et al., «Что программисты делают с наследованием в Java»
См. Также
Ссылки
Последняя правка сделана 2021-05-15 08:20:49
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте