Урок 147. Композиция объектов

   ⁄ 

 Обновлено 6 Июн 2018  ⁄ 

⁄   370

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

В композиции между двумя объектами представлен тип отношения «имеет». Автомобиль «имеет» коробку передач. Ваш компьютер «имеет» центральный процессор. Вы «имеете» сердце. Сложный объект иногда называют целым или родителем. Более простой объект часто называют частью, дочерним элементом или компонентом.

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

Композиция объектов полезна в контексте C++, поскольку позволяет создавать сложные классы, объединяя более простые и легко управляемые части. Это уменьшает сложность и позволяет писать код быстрее и с меньшим количеством ошибок, так как мы можем повторно использовать код, который уже был написан, протестирован и проверен как рабочий.

Типы композиции объектов

Существует два основных подтипа композиции объекта: композиция и агрегация. В этом уроке мы рассмотрим композицию, а агрегацию рассмотрим в следующем.

Примечание по терминологии: Термин «композиция» часто используется для обозначения композиции и агрегации, как единого целого, а не только подтипа композиция. В этом уроке мы будем использовать термин «композиция объекта», когда будем иметь в виду целое (и композицию, и агрегацию), а термин «композицию», когда речь будет идти конкретно о подтипе композиция.



Композиция

Для реализации композиции объект и часть должны иметь следующие отношения:

  часть (член) является частью объекта (класса);

  часть (член) может принадлежать только одному объекту (классу) за раз;

  часть (член) существует, управляемая объектом (классом);

  часть (член) не знает о существовании объекта (класса).

Хорошим примером композиции в жизни является взаимосвязь между телом человека и его сердцем. Рассмотрим это более подробно.

Отношения в композиции — это отношения части-целого. Например, сердце является частью тела человека. Часть в композиции может быть частью только одного объекта за раз. Сердце, которое является частью тела одного человека, не может быть частью тела еще одного человека одновременно.

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

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

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

Наш уже любимый класс Drob является отличным примером композиции:

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

Так как типом отношений в композиции объектов является «имеет» (тело «имеет» сердце, Drob «имеет» m_denominator), то мы можем сказать, что композиция имеет и тип отношения «часть чего-то» (сердце является «частью» тела, m_numerator является «частью» Drob). Композиция часто используется для моделирования физических отношений, где один объект физически находится внутри другого объекта.

Части в композиции могут быть как сингулярными (единственными в своем роде), так и мультипликативными (таких частей может быть несколько). Например, в теле человека есть только одно сердце (сердце является сингулярным), но также 20 пальцев (пальцы являются мультипликативными и могут быть реализованы в виде массива).

Реализация композиций

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



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

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

Еще пример

Во многих играх есть существа или объекты, которые перемещаются по карте или вокруг каких-то объектов. Все эти существа/объекты имеют одну общую вещь — локацию. В следующем примере мы создадим класс Creature, который использует класс Point2D для хранения местоположения (локации) существа.

Сначала создадим класс Point2D. Наше существо будет находиться в измерении 2D, поэтому в нашем классе будет 2 члена: x и y.

Point2D.h:

Обратите внимание, поскольку мы реализовали все наши функции в заголовочном файле (ради сохранения краткости примера), у нас нет Point2D.cpp.

Класс Point2D является целым, который состоит из частей: x и y, время жизни которых напрямую зависит от времени жизни объектов класса Point2D.

Теперь создадим класс Creature. У нашего существа будет 2 свойства: имя (строка) и местоположение (объект класса Point2D).

Creature.h:

Класс Creature также является целым, которое состоит из частей: m_name и m_location, время жизни которых также зависит от времени жизни объектов класса Creature.

И, наконец, main.cpp:

Результат выполнения программы выше:

Enter a name for your creature: Anton
Anton is at (5, 6)
Enter new X location for creature (-1 to quit): 7
Enter new Y location for creature (-1 to quit): 11
Anton is at (7, 11)
Enter new X location for creature (-1 to quit): 2
Enter new Y location for creature (-1 to quit): 4
Anton is at (2, 4)
Enter new X location for creature (-1 to quit): -1

Вариации композиции

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

Например:

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

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

  Композиция может делегировать уничтожение своих частей другому объекту (например, процедуре сбора мусора).

Ключевым моментом является то, что композиция должна управлять своими частями самостоятельно, без вмешательства пользователя композиции.

Композиция и подклассы

Одним из самых частых вопросов, которые задают новички, когда дело доходит до композиции объекта, является: «Когда я должен использовать подкласс вместо непосредственной реализации?». Например, вместо использования класса Point2D для реализации местоположения Creature, мы могли бы просто добавить в класс Creature еще два члена (m_x и m_y) и записать весь код реализации местоположения в классе Creature. Тем не менее, в создании Point2D есть ряд преимуществ:

  Каждый отдельный класс можно сохранить относительно простым/понятным и сфокусировать на выполнении одной конкретной задачи. Таким образом, писать классы легче и понимать их проще. Например, в Point2D всё вертится только вокруг местоположения и это позволяет сохранять общую картину программы более простой.

  Каждый подкласс может быть автономным, что делает его многоразовым. Например, мы можем повторно использовать наш класс Point2D в совершенно другой программе. Или, если нашему Creature когда-либо понадобится еще один пункт в определении локации (например, место куда ему нужно добраться), мы можем просто добавить еще одну переменную-член в Point2D.

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

Хорошим правилом является то, что каждый класс должен выполнять одну конкретную задачу (как в примере с функциями). Этой задачей может быть хранение, манипулирование данными или координация подклассов. Рекомендуем придерживаться этого правила.

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (7 оценок, среднее: 4,71 из 5)
Загрузка...
Подписаться на обновления:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ПОДПИСЫВАЙТЕСЬ

НА КАНАЛ RAVESLI В TELEGRAM

@ravesli

ПОДПИСАТЬСЯ БЕСПЛАТНО