Урок №155. Композиція об’єктів

  Юрій  | 

  Оновл. 26 Вер 2021  | 

 218

У реальному житті складні об’єкти часто складаються з менших, більш простих об’єктів. Наприклад, автомобіль складається з металевої рами, двигуна, 4 коліс, коробки передач, керма та великої кількості інших деталей. Персональний комп’ютер складається з центрального процесора, материнської плати, пам’яті тощо. Навіть ви складаєтеся з невеликих частин: у вас є голова, ноги, руки і т.д. Процес побудови складних об’єктів з більш простих називається композицією об’єкта.

Типи композиції об’єктів

У композиції між двома об’єктами представлений тип відносин «має». Автомобіль «має» коробку передач. Ваш комп’ютер «має» центральний процесор. Ви «маєте» серце. Складний об’єкт іноді називають цілим (або “батьком”). Простіший об’єкт часто називають частиною (або “дочірнім елементом”, “компонентом”).

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

Композиція об’єктів корисна в контексті мови 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 Зірок (4 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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