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

  Юрий  | 

  |

  Обновл. 13 Сен 2021  | 

 61378

 ǀ   6 

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

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

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

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

Композиция объектов полезна в контексте языка 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 есть ряд преимуществ:

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

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

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

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

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

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

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (268 оценок, среднее: 4,84 из 5)
Загрузка...

Комментариев: 6

  1. никита:

    Если так сделать, то получается уже не композиция, точка отдельно, целое ее включает как агрегация?

    Мне кажется в отношениях в примере все-таки реализована агрегация — передача ссылки на точку, которая в принципе может уже существовать, при композиции часть должна создаваться в конструкторе целого

    1. Максим:

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

      В твоем примере:

      Когда создается новый объект класса Point2D (внутри Creature), то он копирует объект p1.
      То есть внутренний объект Point2D (внутри Creature) и отдельный объект p1 — это разные объекты:

  2. Бритва "Бориса":

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

    1. Фото аватара Юрий:

      Урокам уже есть не пару месяцев. В посте выводится дата последнего обновления, а не дата публикации 🙂

  3. kmish:

    Спасибо.
    Знать бы начинающему программисту, как классифицировать необходимость создания подкласса, когда он необходим, а когда нет. В примере этого урока все очевидно и просто, но, кажется мне, не всегда это так… 🙂

    1. Kristian Rhoads:

      Ну тут, мне кажется, решает уже опыт и набитые шишки 🙂

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

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