Композиция объектов. Иерархии целое-часть. Структура простейшей композиции по значению в памяти. Ответственность за уничтожение объектов при композиции

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

Между классом верхнего и нижнего уровня обычно присутствует отношение "целое-часть". Ниже приведен простейший пример композиции — объект двигатель (Engine) является частью объекта автомобиль (Car):

 

С точки зрения памяти, поля дочернего объекта размещаются внутри родительского объекта целиком. К слову, объект std::string m_model также является дочерним объектом, и размещается полностью в родительском объекте Car, с той разницей, что модель - является объектом-значением, а двигатель - объектом-сущностью.

 

В одном объекте может быть несколько дочерних объектов - одного или разных типов. Дочерние объекты могут выделяться как вместе с родительским, так и динамически. При этом родительский класс может быть ответственен либо не ответственен за уничтожение дочерних объектов в зависимости от ситуации. Если доступ к объекту имеет только один класс, то он является ответственным за уничтожение, если же кто-то ещё имеет доступ, то тогда не является ответственным за уничтожение.

Ссылочная композиция. Разрываемая композиция. Кратность композиции. Одиночная, множественная и недетерминированная кратность.

Ссылочная композиция.

Некоторые виды композиции не предполагают ответственности на уничтожение д

дочернего объекта. Такая связь скорее является логическим объединением в группу, нежели жестким отношением целое-часть. Для дочерних объектов в логических объединениях свойственно вхождение сразу в несколько родительских объектов. В таком случае за уничтожение дочернего объекта отвечает кто-либо другой, а не родительский объект.

Если такая связь между объектами является постоянной, то, как правило, дочерний объект хранится по ссылке либо константной ссылке, которая передается в родительский объект при конструировании. Обычно, таким видам объектов не свойственно копирование и присвоение, и эти операции имеет смысл в явном виде запретить.

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

Разрываемая композиция

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

Ниже представлен класс, моделирующий вертолет (Helicopter). Вертолет имеет уникальный числовой номер. Вертолет находится в конкретный момент времени по конкретным координатам и его нос направлен под конкретным углом относительно осей. На вертолет может быть установлено необязательное к наличию орудие (объект Weapon). При наличии орудия, вертолет может производить выстрелы, в противном случае — только летать.

С точки зрения хранения данных в памяти, единственным способом для реализации такой необязательной связи между объектами является хранение указателя на дочерний объект в памяти родительского. Указатель будет адресовать конкретный дочерний объект, если связь установлена. При отсутствии связи значение указателя будет равно nullptr.

Связь между объектами Helicopter и Weapon не является обязательной. Она может устанавливаться внешне через вызов метода installWeapon, а затем разрываться через вызов deinstallWeapon. Во избежание ошибок объект-вертолет должен проверять, что может быть установлено только одно орудие, а также гарантировать, что снять орудие можно только после его установки. Если орудие прикреплено к вертолету на момент его уничтожения, орудие уничтожается вместе с ним.

Кратность композиции.

Одиночная кратность (связь один-ко-одному) Car-Engine

Множественная кратность(один-ко-многим) (HelicopterEscadrille-Helicopter).

Недетерминированная кратность (многие-ко-многим) (HelicopterPilot-MilitaryMission).

 

 

23. Применение контейнера std::vector для композиции с недетерминированной кратностью. Композиция объектов-значений и объектов-сущностей.

 

Вектор является одним из классов-контейнеров стандартной библиотеки шаблонов (STL - Standard Template Library). STL-контейнеры, такие как вектор, представляют собой реализацию классических структур данных, ранее рассмотренных в обеспечивающей дисциплине. Подобные мощные утилитарные классы для классических структур имеются во всех современных языках программирования, и С++ не является исключением.

 

Простейшие типовые операции, характерные векторам, доступны через методы класса std::vector, среди них такие массово используемые во многих программах:

● size - возвращает число хранимых элементов;

● empty - выясняет пуст ли вектор;

● clear - очищает вектор;

● push_back - добавляет новый элемент в конец вектора

● pop_back - удаляет элемент с конца вектора;

● оператор индексной выборки [] - доступ к элементу с конкретным индексом без проверки;

● at - доступ к элементу с конкретным индексом с проверкой на выход за границу;

● insert - вставляет элемент в указанную конкретную позицию (с автоматическим сдвигом);

● erase - удаляет элемент из указанной конкретной позиции (с автоматическим сдвигом)

 

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

 

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

● нет необходимости в передаче и извлечении максимального числа объектов;

● исчезает необходимость в явном деструкторе, поскольку деструктор вектора, который освободит выделенный блок памяти, будет автоматически вызван из генерируемого компилятором автоматического деструктора класса;

● исчезает необходимость в запрещении конструктора копий, поскольку реализация класса std::vector обладает функциональностью корректного копирования своего содержимого, и никаких "висячих" указателей в автоматическом конструкторе копий не возникнет;

● вектору также можно доверить проверку индексов при доступе к конкретным объектам.

 

 

Основная отличительная особенность любых объектов-значений - возможность свободного копирования и присвоение подобно встроенным типам. Наличие у объектов-точек такой возможности позволило легко поместить их в вектор.

 

Часто существует необходимость во множественной композиции объектов-сущностей, которые, как правило, не предполагают или даже полностью запрещают копирование, поскольку двух одинаковых объектов-сущностей в программе одновременно существовать не должно. Из невозможности копирования объектов вытекает невозможность помещения объектов в вектор. Также в вектор нельзя помещать ссылки на объекты, поскольку ссылка не является копируемой в принципе. Очевидно, в таком случае остается лишь одно решение - вместо объектов или ссылок на объекты в вектор следует поместить указатели на объекты. Указатели могут копироваться и имеют значение по умолчанию не зависимо от типа данного (nullptr).

 

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

 

Как видно из приведенной реализации, внедрение векторов вместо массивов позволило также достичь ряда заметных упрощений, среди которых следующие:

● полностью исчезла какая-либо необходимость в собственном конструкторе, т.к. более нет потребности передавать максимальный размер эскадрильи, аналогично, был убран метод getMaxUnitsCount, смысл которого теперь утерян;

● реализация больше не хранит нулевые указатели для сбитых или выведенных из состава эскадрильи вертолетов, соответственно нет нужды в методе findFreeUnitPosition;

● метод getHelicopter возвращает ссылку на объект-вертолет вместо указателя, поскольку нулевых указателей внутри вектора теперь не хранится;

● аналогично примерам о журнале полета и истории приземлений вертолетов на площадке, проверку индексов при доступе берет на себя вектор;

● реализация метода getJoinedUnitsCount упрощается до запроса количества размещенных элементов внутреннего вектора;

● при добавлении вертолета в эскадрилью его адрес просто помещается в конец вектора;

● при исключении вертолета из состава эскадрильи используется готовый алгоритм метода erase с автоматическим сдвигом, что было возможно в предыдущей реализации, но не привлекало громоздкостью ручного кодирования в явном виде.