stringtranslate.com

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

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

Композиция вместо наследования (или принцип составного повторного использования ) в объектно-ориентированном программировании (ООП) — это принцип, согласно которому классы должны отдавать предпочтение полиморфному поведению и повторному использованию кода посредством их композиции (путем содержания экземпляров других классов, реализующих желаемую функциональность) вместо наследования от базового или родительского класса. [2] В идеале все повторное использование может быть достигнуто путем сборки существующих компонентов, но на практике наследование часто требуется для создания новых. Поэтому наследование и композиция объектов обычно работают рука об руку, как обсуждалось в книге Design Patterns (1994). [3]

Основы

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

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

Пример

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

Ниже приведен пример на языке C++ :

class Object { public : virtual void update () { // no-op }        виртуальная пустота draw () { // без операции }      virtual void collide ( Object objects []) { // no-op } };      класс Видимый : публичный объект { Модель * модель ;      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 { // код для обновления положения этого объекта } };           

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

Обратите внимание, что множественное наследование опасно, если реализовано неаккуратно, поскольку может привести к проблеме ромба . Одним из решений этой проблемы является создание таких классов, как VisibleAndSolid, VisibleAndMovable, VisibleAndSolidAndMovable, и т. д. для каждой необходимой комбинации; однако это приводит к большому количеству повторяющегося кода. C++ использует виртуальное наследование для решения проблемы ромба множественного наследования.

Состав и интерфейсы

Примеры C++ в этом разделе демонстрируют принцип использования композиции и интерфейсов для достижения повторного использования кода и полиморфизма. Поскольку в языке C++ нет выделенного ключевого слова для объявления интерфейсов, следующий пример C++ использует наследование от чистого абстрактного базового класса . Для большинства целей это функционально эквивалентно интерфейсам, предоставляемым в других языках, таких как Java [4] : 87  и C#. [5] : 144 

Введем абстрактный класс с именем VisibilityDelegate, с подклассами NotVisibleи Visible, который предоставляет средства рисования объекта:

класс VisibilityDelegate { public : virtual void 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, который предоставляет средства перемещения объекта:

класс UpdateDelegate { public : virtual void update () = 0 ; };      class NotMovable : public UpdateDelegate { public : virtual void update () override { // no-op } };           class Movable : public UpdateDelegate { public : virtual void update () override { // код для обновления положения этого объекта } };           

Введем абстрактный класс с именем CollisionDelegate, с подклассами NotSolidи Solid, который предоставляет средства столкновения с объектом:

class CollisionDelegate { public : virtual void collide ( Object objects []) = 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 ;       public : Object ( VisibilityDelegate * v , UpdateDelegate * u , CollisionDelegate * c ) : _v ( v ) , _u ( u ) , _c ( c ) {}              void обновление () { _u -> обновление (); }     void draw () { _v -> draw (); }     void collide ( Object объекты []) { _c -> collide ( объекты ); } };     

Тогда конкретные классы будут выглядеть так:

класс Игрок : публичный Объект { публичный : Игрок () : Объект ( новый Видимый (), новый Подвижный (), новый Твердый ()) {}              // ... };класс Дым : публичный Объект { публичный : Дым () : Объект ( новый Видимый (), новый Подвижный (), новый НеТвердый ()) {}              // ... };

Преимущества

Отдавать предпочтение композиции, а не наследованию — это принцип проектирования, который дает проекту большую гибкость. Более естественно создавать классы бизнес-домена из различных компонентов, чем пытаться найти между ними общность и создавать генеалогическое древо. Например, педаль акселератора и рулевое колесо имеют очень мало общих черт , но оба являются жизненно важными компонентами автомобиля. То, что они могут делать, и как их можно использовать для пользы автомобиля, легко определить. Композиция также обеспечивает более стабильную бизнес-домен в долгосрочной перспективе, поскольку она менее подвержена причудам членов семьи. Другими словами, лучше составить то, что объект может делать ( has-a ), чем расширить то, чем он является ( is-a ). [1]

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

Некоторые языки, в частности Go [6] и Rust [7] , используют исключительно композицию типов.

Недостатки

Одним из распространенных недостатков использования композиции вместо наследования является то, что методы, предоставляемые отдельными компонентами, возможно, придется реализовать в производном типе, даже если они являются только методами пересылки (это справедливо для большинства языков программирования, но не для всех; см. § Избежание недостатков). Напротив, наследование не требует повторной реализации всех методов базового класса в производном классе. Вместо этого производному классу нужно реализовать (переопределить) только методы, имеющие поведение, отличное от поведения методов базового класса. Это может потребовать значительно меньше усилий по программированию, если базовый класс содержит много методов, обеспечивающих поведение по умолчанию, и только некоторые из них нужно переопределить в производном классе.

Например, в коде C# ниже переменные и методы базового Employeeкласса наследуются подклассами HourlyEmployeeи SalariedEmployeeпроизводными. Только Pay()метод должен быть реализован (специализирован) каждым производным подклассом. Другие методы реализуются самим базовым классом и являются общими для всех его производных подклассов; их не нужно повторно реализовывать (переопределять) или даже упоминать в определениях подкласса.

UML-класс Employee.svg

// Базовый класс public abstract class Employee { // Свойства protected string Name { get ; set ; } protected int ID { get ; set ; } protected decimal PayRate { get ; set ; } protected int HoursWorked { get ; }                                // Получить оплату за текущий платежный период 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 ; } }                  

Избегание недостатков

Этого недостатка можно избежать, используя черты , миксины , встраивание (типов) или расширения протоколов .

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

Эмпирические исследования

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

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

—  Темперо и др. , «Что программисты делают с наследованием в Java» [23]

Смотрите также

Ссылки

  1. ^ ab Freeman, Eric; Robson, Elizabeth; Sierra, Kathy; Bates, Bert (2004). Head First Design Patterns . O'Reilly. стр. 23. ISBN 978-0-596-00712-6.
  2. ^ Knoernschild, Kirk (2002). Java Design - Objects, UML, and Process: 1.1.5 Composite Reuse Principle (CRP). Addison-Wesley Inc. ISBN 9780201750447. Получено 29.05.2012 .
  3. ^ Гамма, Эрих ; Хелм, Ричард; Джонсон, Ральф ; Влиссидес, Джон (1994). Шаблоны проектирования: элементы повторно используемого объектно-ориентированного программного обеспечения . Addison-Wesley . стр. 20. ISBN 0-201-63361-2. OCLC  31171684.
  4. ^ ab Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (третье изд.). Addison-Wesley. ISBN 978-0134685991.
  5. ^ ab Price, Mark J. (2022). C# 8.0 и .NET Core 3.0 — современная кроссплатформенная разработка: создание приложений с C#, .NET Core, Entity Framework Core, ASP.NET Core и ML.NET с использованием Visual Studio Code . Пакет. ISBN 978-1-098-12195-2.
  6. ^ Пайк, Роб (2012-06-25). "Меньше значит экспоненциально больше" . Получено 2016-10-01 .
  7. ^ "Характеристики объектно-ориентированных языков - язык программирования Rust". doc.rust-lang.org . Получено 10.10.2022 .
  8. ^ "Что нового в C# 8.0". Microsoft Docs . Microsoft . Получено 2019-02-20 .
  9. ^ Скит, Джон (23 марта 2019 г.). C# in Depth . Мэннинг. ISBN 978-1617294532.
  10. ^ Альбахари, Джозеф (2022). Кратко о C#10 . О'Рейли. ISBN 978-1-098-12195-2.
  11. ^ "Alias ​​This". Справочник языка D. Получено 15.06.2019 .
  12. ^ "(Тип) Встраивание". Документация по языку программирования Go . Получено 10.05.2019 .
  13. ^ https://projectlombok.org [ пустой URL ]
  14. ^ "@Delegate". Проект Ломбок . Получено 2018-07-11 .
  15. ^ "MikeInnes/Lazy.jl". GitHub .
  16. ^ "JeffreySarnoff/TypedDelegation.jl". GitHub .
  17. ^ "Метод пересылки макроса". JuliaLang . 20 апреля 2019 г. Получено 18 августа 2022 г.
  18. ^ "Делегированные свойства". Справочник Kotlin . JetBrains . Получено 11 июля 2018 г.
  19. ^ "PHP: Traits". www.php.net . Получено 23 февраля 2023 г. .
  20. ^ "Система типов". docs.raku.org . Получено 18 августа 2022 г. .
  21. ^ "Экспортные положения". Документация Scala . Получено 2021-10-06 .
  22. ^ "Протоколы". Язык программирования Swift . Apple Inc. Получено 11 июля 2018 г.
  23. ^ Темперо, Эван; Янг, Хонг Юл; Нобл, Джеймс (2013). «Что программисты делают с наследованием в Java» (PDF) . ECOOP 2013 – Объектно-ориентированное программирование . ECOOP 2013–Объектно-ориентированное программирование. Конспект лекций по информатике. Том 7920. С. 577–601. doi :10.1007/978-3-642-39038-8_24. ISBN 978-3-642-39038-8.