Композиция над наследованием - Composition over inheritance

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

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

Основы

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

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

Пример

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

Пример в C ++ следует:

учебный класс Объект{общественный:    виртуальный пустота Обновить() {        // без операции    }    виртуальный пустота рисовать() {        // без операции    }    виртуальный пустота столкнуться(Объект объекты[]) {        // без операции    }};учебный класс Видимый : общественный Объект{    Модель* модель;общественный:    виртуальный пустота рисовать() отменять {        // код для рисования модели в позиции этого объекта    }};учебный класс Твердый : общественный Объект{общественный:    виртуальный пустота столкнуться(Объект объекты[]) отменять {        // код для проверки и реагирования на столкновения с другими объектами    }};учебный класс Подвижный : общественный Объект{общественный:    виртуальный пустота Обновить() отменять {        // код для обновления позиции этого объекта    }};

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

  • учебный класс Игрок - который Твердый, Подвижный и Видимый
  • учебный класс Облако - который Подвижный и Видимый, но нет Твердый
  • учебный класс Строительство - который Твердый и Видимый, но нет Подвижный
  • учебный класс Ловушка - который Твердый, но ни Видимый ни Подвижный

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

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

Примеры C ++ в этом разделе демонстрируют принцип использования композиции и интерфейсов для повторного использования кода и полиморфизма. Поскольку в языке C ++ нет специального ключевого слова для объявления интерфейсов, в следующем примере C ++ используется «наследование от чистого абстрактного базового класса». Для большинства целей это функционально эквивалентно интерфейсам, предоставляемым на других языках, таких как Java и C #.

Представьте абстрактный класс с именем ВидимостьДелегат, с подклассами Невидимый и Видимый, который предоставляет средства рисования объекта:

учебный класс ВидимостьДелегат{общественный:    виртуальный пустота рисовать() = 0;};учебный класс Невидимый : общественный ВидимостьДелегат{общественный:    виртуальный пустота рисовать() отменять {        // без операции    }};учебный класс Видимый : общественный ВидимостьДелегат{общественный:    виртуальный пустота рисовать() отменять {        // код для рисования модели в позиции этого объекта    }};

Представьте абстрактный класс с именем UpdateDelegate, с подклассами NotMovable и Подвижный, который позволяет перемещать объект:

учебный класс UpdateDelegate{общественный:    виртуальный пустота Обновить() = 0;};учебный класс NotMovable : общественный UpdateDelegate{общественный:    виртуальный пустота Обновить() отменять {        // без операции    }};учебный класс Подвижный : общественный UpdateDelegate{общественный:    виртуальный пустота Обновить() отменять {        // код для обновления позиции этого объекта    }};

Представьте абстрактный класс с именем СтолкновениеДелегат, с подклассами NotSolid и Твердый, который обеспечивает средство столкновения с объектом:

учебный класс СтолкновениеДелегат{общественный:    виртуальный пустота столкнуться(Объект объекты[]) = 0;};учебный класс NotSolid : общественный СтолкновениеДелегат{общественный:    виртуальный пустота столкнуться(Объект объекты[]) отменять {        // без операции    }};учебный класс Твердый : общественный СтолкновениеДелегат{общественный:    виртуальный пустота столкнуться(Объект объекты[]) отменять {        // код для проверки и реагирования на столкновения с другими объектами    }};

Наконец, представьте класс с именем Объект с членами для управления его видимостью (используя ВидимостьДелегат), подвижность (с использованием UpdateDelegate) и твердость (используя СтолкновениеДелегат). У этого класса есть методы, которые делегируют его членам, например Обновить() просто вызывает метод на UpdateDelegate:

учебный класс Объект{    ВидимостьДелегат* _v;    UpdateDelegate* _u;    СтолкновениеДелегат* _c;общественный:    Объект(ВидимостьДелегат* v, UpdateDelegate* ты, СтолкновениеДелегат* c)        : _v(v)        , _u(ты)        , _c(c)    {}    пустота Обновить() {        _u->Обновить();    }    пустота рисовать() {        _v->рисовать();    }    пустота столкнуться(Объект объекты[]) {        _c->столкнуться(объекты);    }};

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

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

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

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

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

Некоторые языки, особенно Идти используйте исключительно шрифтовую композицию.[4]

Недостатки

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

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

// Базовый классобщественный Абстрактные учебный класс Наемный рабочий{    // Характеристики    защищенный нить Имя { получать; набор; }    защищенный int Я БЫ { получать; набор; }    защищенный десятичный Ставка оплаты { получать; набор; }    защищенный int Отработанные часы { получать; }    // Получаем оплату за текущий платежный период    общественный Абстрактные десятичный Платить();}// Производный подклассобщественный учебный класс Почасовая : Наемный рабочий{    // Получаем оплату за текущий платежный период    общественный отменять десятичный Платить()    {        // отработанное время в часах        возвращаться Отработанные часы * Ставка оплаты;    }}// Производный подклассобщественный учебный класс Наемный работник : Наемный рабочий{    // Получаем оплату за текущий платежный период    общественный отменять десятичный Платить()    {        // Ставка оплаты - это годовая зарплата, а не почасовая ставка        возвращаться Отработанные часы * Ставка оплаты / 2087;    }}

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

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

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

  • C # предоставляет методы интерфейса по умолчанию, начиная с версии 8.0, что позволяет определять тело элемента интерфейса. [5]
  • D предоставляет явное объявление псевдонима this внутри типа, которое может направлять в него каждый метод и член другого содержащегося типа. [6]
  • Идти встраивание типов позволяет избежать использования методов пересылки.[7]
  • Ява предоставляет Project Lombok[8] что позволяет реализовать делегирование с помощью одного @Delegate аннотации к полю вместо копирования и сохранения имен и типов всех методов из делегированного поля.[9] Java 8 позволяет использовать методы по умолчанию в интерфейсе, аналогично C # и т. Д.
  • Юля макросы могут использоваться для создания методов пересылки. Существует несколько реализаций, таких как Lazy.jl и TypedDelegation.jl.
  • Котлин включает шаблон делегирования в синтаксис языка.[10]
  • Раку обеспечивает ручки ключевое слово для облегчения пересылки методов.
  • Ржавчина предоставляет черты с реализациями по умолчанию.
  • Быстрый Расширения могут использоваться для определения реализации протокола по умолчанию в самом протоколе, а не в реализации отдельного типа.[11]

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

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

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

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

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

Рекомендации

  1. ^ а б Фриман, Эрик; Робсон, Элизабет; Сьерра, Кэти; Бейтс, Берт (2004). Шаблоны проектирования Head First. О'Рейли. п.23. ISBN  978-0-596-00712-6.
  2. ^ Кнерншильд, Кирк (2002). Дизайн Java - Объекты, UML и Процесс: 1.1.5 Принцип многократного использования композитов (CRP). Addison-Wesley Inc. ISBN  9780201750447. Получено 2012-05-29.
  3. ^ Гамма, Эрих; Хелм, Ричард; Джонсон, Ральф; Влиссидес, Джон (1994). Паттерны проектирования: элементы объектно-ориентированного программного обеспечения многократного использования. Эддисон-Уэсли. п.20. ISBN  0-201-63361-2. OCLC  31171684.
  4. ^ Пайк, Роб (2012-06-25). "Меньше значит экспоненциально больше". Получено 2016-10-01.
  5. ^ «Что нового в C # 8.0». Документы Microsoft. Microsoft. Получено 2019-02-20.
  6. ^ "Псевдоним Это". Справочник по языку D. Получено 2019-06-15.
  7. ^ "(Тип) Встраивание ". Документация по языку программирования Go. Получено 2019-05-10.
  8. ^ https://projectlombok.org
  9. ^ "@Delegate". Проект Ломбок. Получено 2018-07-11.
  10. ^ «Делегированные свойства». Ссылка на Котлин. JetBrains. Получено 2018-07-11.
  11. ^ «Протоколы». Язык программирования Swift. Apple Inc.. Получено 2018-07-11.
  12. ^ Темперо, Эван; Ян, Хун Юл; Благородный, Джеймс (2013). Что программисты делают с наследованием в Java (PDF). ECOOP 2013 – объектно-ориентированное программирование. С. 577–601.