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

Композиция вместо наследования (или принцип составного повторного использования ) в объектно-ориентированном программировании (ООП) — это принцип, согласно которому классы должны отдавать предпочтение полиморфному поведению и повторному использованию кода посредством своей композиции (путем содержания экземпляров других классов, реализующих желаемую функциональность) вместо наследования от базы . или родительский класс. [2] В идеале любое повторное использование может быть достигнуто путем сборки существующих компонентов, но на практике для создания новых часто требуется наследование. Поэтому наследование и композиция объектов обычно работают рука об руку, как описано в книге « Шаблоны проектирования» (1994). [3]
Основы
[ редактировать ]Реализация композиции вместо наследования обычно начинается с создания различных интерфейсов, представляющих поведение, которое должна демонстрировать система. Интерфейсы могут способствовать полиморфному поведению. Классы, реализующие идентифицированные интерфейсы, создаются и добавляются в классы предметной области бизнеса по мере необходимости. Таким образом, поведение системы реализуется без наследования.
Фактически, все классы бизнес- предметной области могут быть базовыми классами вообще без какого-либо наследования. Альтернативная реализация поведения системы достигается путем предоставления другого класса, реализующего желаемый интерфейс поведения. Класс, содержащий ссылку на интерфейс, может поддерживать реализации интерфейса — выбор, который можно отложить до времени выполнения .
Пример
[ редактировать ]Наследование
[ редактировать ]Ниже приведен пример на C++ :
class Object
{
public:
virtual void update() {
// no-op
}
virtual void draw() {
// no-op
}
virtual void collide(Object objects[]) {
// no-op
}
};
class Visible : public Object
{
Model* model;
public:
virtual void draw() override {
// code to draw a model at the position of this object
}
};
class Solid : public Object
{
public:
virtual void collide(Object objects[]) override {
// code to check for and react to collisions with other objects
}
};
class Movable : public Object
{
public:
virtual void update() override {
// code to update the position of this object
}
};
Затем предположим, что у нас также есть эти конкретные классы:
- сорт
Player
- что такоеSolid
,Movable
иVisible
- сорт
Cloud
- что такоеMovable
иVisible
, но неSolid
- сорт
Building
- что такоеSolid
иVisible
, но неMovable
- сорт
Trap
- что такоеSolid
, но ниVisible
ниMovable
Обратите внимание, что множественное наследование опасно, если его не реализовать осторожно, поскольку оно может привести к проблеме ромба . Одним из решений этой проблемы является создание таких классов, как VisibleAndSolid
, VisibleAndMovable
, VisibleAndSolidAndMovable
и т. д. для каждой необходимой комбинации; однако это приводит к большому количеству повторяющегося кода. C++ использует виртуальное наследование для решения алмазной проблемы множественного наследования.
Состав и интерфейсы
[ редактировать ]Примеры C++ в этом разделе демонстрируют принцип использования композиции и интерфейсов для повторного использования кода и полиморфизма. Поскольку в языке C++ нет специального ключевого слова для объявления интерфейсов, в следующем примере C++ используется наследование от чисто абстрактного базового класса . Для большинства целей это функционально эквивалентно интерфейсам, предоставляемым на других языках, таких как Java. [4] : 87 и С#. [5] : 144
Введем абстрактный класс с именем VisibilityDelegate
, с подклассами NotVisible
и Visible
, который предоставляет средства рисования объекта:
class 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 {
// code to draw a model at the position of this object
}
};
Введем абстрактный класс с именем UpdateDelegate
, с подклассами NotMovable
и Movable
, который предоставляет средства перемещения объекта:
class 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 {
// code to update the position of this object
}
};
Введем абстрактный класс с именем 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 {
// code to check for and react to collisions with other objects
}
};
Наконец, представьте класс с именем Object
с участниками для контроля его видимости (с помощью VisibilityDelegate
), подвижность (с помощью UpdateDelegate
) и прочность (с использованием CollisionDelegate
). Этот класс имеет методы, которые делегируют его членам, например update()
просто вызывает метод UpdateDelegate
:
class Object
{
VisibilityDelegate* _v;
UpdateDelegate* _u;
CollisionDelegate* _c;
public:
Object(VisibilityDelegate* v, UpdateDelegate* u, CollisionDelegate* c)
: _v(v)
, _u(u)
, _c(c)
{}
void update() {
_u->update();
}
void draw() {
_v->draw();
}
void collide(Object objects[]) {
_c->collide(objects);
}
};
Тогда конкретные классы будут выглядеть так:
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 ). [1]
Первоначальный проект упрощается за счет определения поведения системных объектов в отдельных интерфейсах вместо создания иерархических отношений для распределения поведения между классами бизнес-домена посредством наследования. Этот подход легче учитывает будущие изменения требований, которые в противном случае потребовали бы полной реструктуризации классов бизнес- предметной области в модели наследования. Кроме того, он позволяет избежать проблем, часто связанных с относительно небольшими изменениями в модели на основе наследования, включающей несколько поколений классов. Отношение композиции более гибкое, поскольку его можно изменить во время выполнения, тогда как отношения подтипирования являются статическими и требуют перекомпиляции во многих языках.
Некоторые языки, особенно Go [6] и Ржавчина , [7] используйте исключительно композицию типов.
Недостатки
[ редактировать ]Одним из распространенных недостатков использования композиции вместо наследования является то, что методы, предоставляемые отдельными компонентами, возможно, придется реализовать в производном типе, даже если они являются только методами пересылки (это верно для большинства языков программирования, но не для всех; см. § Избегание недостатки ). Напротив, наследование не требует повторной реализации всех методов базового класса в производном классе. Скорее, производному классу нужно только реализовать (переопределить) методы, поведение которых отличается от методов базового класса. Это может потребовать значительно меньше усилий по программированию, если базовый класс содержит множество методов, обеспечивающих поведение по умолчанию, и только некоторые из них необходимо переопределить в производном классе.
Например, в приведенном ниже коде C# переменные и методы класса Employee
базовый класс наследуются HourlyEmployee
и SalariedEmployee
производные подклассы. Только Pay()
метод должен быть реализован (специализирован) каждым производным подклассом. Остальные методы реализуются самим базовым классом и используются всеми его производными подклассами; их не нужно переопределять (переопределять) или даже упоминать в определениях подклассов.
// Base class
public abstract class Employee
{
// Properties
protected string Name { get; set; }
protected int ID { get; set; }
protected decimal PayRate { get; set; }
protected int HoursWorked { get; }
// Get pay for the current pay period
public abstract decimal Pay();
}
// Derived subclass
public class HourlyEmployee : Employee
{
// Get pay for the current pay period
public override decimal Pay()
{
// Time worked is in hours
return HoursWorked * PayRate;
}
}
// Derived subclass
public class SalariedEmployee : Employee
{
// Get pay for the current pay period
public override decimal Pay()
{
// Pay rate is annual salary instead of hourly rate
return HoursWorked * PayRate / 2087;
}
}
Как избежать недостатков
[ редактировать ]Этого недостатка можно избежать, используя черты , примеси (типа) , встраивание или расширения протокола .
Некоторые языки предоставляют специальные средства для смягчения этого:
- C# предоставляет методы интерфейса по умолчанию, начиная с версии 8.0, которые позволяют определять тело для члена интерфейса. [8] [5] : 28–29 [9] : 38 [10] : 466–468
- D предоставляет явное объявление «псевдоним this» внутри типа, в который можно пересылать каждый метод и член другого содержащегося типа. [11]
- Dart предоставляет примеси с реализациями по умолчанию, которыми можно делиться.
- Встраивание типа Go позволяет избежать необходимости использования методов пересылки. [12]
- Java предоставляет методы интерфейса по умолчанию, начиная с версии 8. [4] : 104 Проект Ломбок [13] поддерживает делегирование с помощью
@Delegate
аннотацию к полю вместо копирования и сохранения имен и типов всех методов из делегированного поля. [14] - Макросы Julia можно использовать для создания методов пересылки. Существует несколько реализаций, таких как Lazy.jl. [15] и TypedDelegation.jl. [16] [17]
- Котлин включает шаблон делегирования в синтаксис языка. [18]
- PHP поддерживает черты , начиная с PHP 5.4. [19]
- Раку предоставляет
handles
черта, облегчающая пересылку методов. [20] - Rust предоставляет трейты с реализациями по умолчанию.
- Scala (начиная с версии 3) предоставляет предложение «экспорт» для определения псевдонимов для выбранных членов объекта. [21]
- Расширения Swift можно использовать для определения реализации протокола по умолчанию в самом протоколе, а не в реализации отдельного типа. [22]
Эмпирические исследования
[ редактировать ]Исследование 93 Java-программ с открытым исходным кодом (разного размера), проведенное в 2013 году, показало, что:
Хотя не существует огромной возможности заменить наследование композицией (...), эта возможность значительна (в среднем 2% использований [наследования] представляют собой только внутреннее повторное использование, а еще 22% — только внешнее или внутреннее повторное использование). Наши результаты показывают, что нет необходимости беспокоиться о злоупотреблении наследованием (по крайней мере, в программном обеспечении Java с открытым исходным кодом), но они выдвигают на первый план вопрос об использовании композиции по сравнению с наследованием. Если существуют значительные затраты, связанные с использованием наследования, когда можно использовать композицию, то наши результаты предполагают, что есть некоторый повод для беспокойства.
- Темперо и др. , «Что программисты делают с наследованием в Java» [23]
См. также
[ редактировать ]- Шаблон делегирования
- Принцип замены Лискова
- Объектно-ориентированный дизайн
- Состав объекта
- Ролевое программирование
- Образец состояния
- Паттерн стратегии
Ссылки
[ редактировать ]- ^ Jump up to: а б Фриман, Эрик; Робсон, Элизабет; Сьерра, Кэти; Бейтс, Берт (2004). Шаблоны проектирования Head First . О'Рейли. п. 23 . ISBN 978-0-596-00712-6 .
- ^ Кнёрншильд, Кирк (2002). Проектирование Java — объекты, UML и процессы: 1.1.5 Принцип составного повторного использования (CRP) . Addison-Wesley Inc. ISBN 9780201750447 . Проверено 29 мая 2012 г.
- ^ Гамма, Эрих ; Хелм, Ричард; Джонсон, Ральф ; Влиссидес, Джон (1994). Шаблоны проектирования: элементы объектно-ориентированного программного обеспечения многократного использования . Аддисон-Уэсли . п. 20 . ISBN 0-201-63361-2 . ОСЛК 31171684 .
- ^ Jump up to: а б Блох, Джошуа (2018). «Эффективная Java: Руководство по языку программирования» (третье изд.). Аддисон-Уэсли. ISBN 978-0134685991 .
- ^ Jump up to: а б Прайс, Марк Дж. (2022). C# 8.0 и .NET Core 3.0 — современная кроссплатформенная разработка: создавайте приложения с помощью C#, .NET Core, Entity Framework Core, ASP.NET Core и ML.NET с помощью кода Visual Studio . Пакет. ISBN 978-1-098-12195-2 .
- ^ Пайк, Роб (25 июня 2012 г.). «Меньше значит экспоненциально больше» . Проверено 1 октября 2016 г.
- ^ «Характеристики объектно-ориентированных языков — язык программирования Rust» . doc.rust-lang.org . Проверено 10 октября 2022 г.
- ^ «Что нового в C# 8.0» . Документы Майкрософт . Майкрософт . Проверено 20 февраля 2019 г.
- ^ Скит, Джон (23 марта 2019 г.). C# в глубине . Мэннинг. ISBN 978-1617294532 .
- ^ Альбахари, Джозеф (2022). C# 10 в двух словах О'Рейли. ISBN 978-1-098-12195-2 .
- ^ «Этот псевдоним» . D Справочник по языку . Проверено 15 июня 2019 г.
- ^ « (Тип) Встраивание» . Документация по языку программирования Go . Проверено 10 мая 2019 г.
- ^ https://projectlombok.org
- ^ "@Делегат" . Проект Ломбок . Проверено 11 июля 2018 г.
- ^ "MikeInnes/Lazy.jl" . Гитхаб .
- ^ «ДжеффриСарнофф/TypedDelegation.jl» . Гитхаб .
- ^ «Макрос пересылки метода» . Джулия Ланг . 20 апреля 2019 г. Проверено 18 августа 2022 г.
- ^ «Делегированные свойства» . Справочник по Котлину . ДжетБрэйнс . Проверено 11 июля 2018 г.
- ^ «PHP: Черты» . www.php.net . Проверено 23 февраля 2023 г.
- ^ «Система типов» . docs.raku.org . Проверено 18 августа 2022 г.
- ^ «Экспортные положения» . Документация Скала . Проверено 6 октября 2021 г.
- ^ «Протоколы» . Язык программирования Swift . Apple Inc. Проверено 11 июля 2018 г.
- ^ Темперо, Юэн; Ян, Хун Юл; Ноубл, Джеймс (2013). «Что программисты делают с наследованием в Java» (PDF) . ЭКООП 2013 – Объектно-ориентированное программирование . ЭКООП 2013–Объектно-ориентированное программирование. Конспекты лекций по информатике. Том. 7920. стр. 577–601. дои : 10.1007/978-3-642-39038-8_24 . ISBN 978-3-642-39038-8 .