На попередніх уроках ми розглянули два підтипи композиції об’єктів: композицію і агрегацію. Композиція об’єктів використовується для моделювання відносин, в яких складний об’єкт (ціле) складається з декількох простих об’єктів (частин).
На цьому уроці ми розглянемо наступний тип відносин між двома незв’язаними об’єктами — асоціацію. На відміну від композиції об’єктів, в асоціації немає відносин “частин-цілого”.
Асоціація
В асоціації два незв’язаних об’єкти повинні відповідати наступним відносинам:
Перший об’єкт (член) не зв’язаний з другим об’єктом (класом).
Перший об’єкт (член) одночасно може належати відразу декільком об’єктам (класам).
Перший об’єкт (член) існує, НЕ керований другим об’єктом (класом).
Перший об’єкт (член) може знати або не знати про існування другого об’єкта (класу).
На відміну від композиції або агрегації, де частина є частиною цілого, в асоціації об’єкти між собою ніяк не пов’язані. Подібно агрегації, перший об’єкт одночасно може належати відразу декільком об’єктам і не керується ними. Однак, на відміну від агрегації, де відносини односпрямовані, в асоціації відносини можуть бути як односпрямованими, так і двонаправленими (коли обидва об’єкти знають про існування один одного).
Відносини між лікарями і пацієнтами — це відмінний приклад асоціації. Лікар пов’язаний з пацієнтом, але ці відносини не можна назвати відносинами “частин-цілого”. Лікар може приймати десятки пацієнтів за день, а пацієнт може звертатися до декількох лікарів.
Ми можемо сказати, що типом відносин в асоціації є «використовує». Лікар «використовує» пацієнта для отримання доходу. Пацієнт «використовує» лікаря, щоб вилікувати хворобу або поліпшити своє самопочуття.
Реалізація асоціацій
Асоціації реалізуються по-різному. Однак найчастіше вони реалізуються через вказівники, де класи вказують на об’єкти один одного.
У наступному прикладі ми реалізуємо двонаправлені відносини між Лікарем і Пацієнтом, тому що Лікар повинен знати своїх Пацієнтів в обличчя, а Пацієнти можуть звертатися до різних Лікарів:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
#include <iostream> #include <string> #include <vector> // Оскільки відносини між цими класами двонаправлені, то для класу Doctor тут потрібно використати попереднє оголошення class Doctor; class Patient { private: std::string m_name; std::vector<Doctor *> m_doctor; // завдяки вищенаведеному попередньому оголошенню Doctor, цей рядок не викличе помилку компіляції // Ми оголошуємо метод addDoctor() закритим, тому що не хочемо його відкритого використання. // Доступ до нього відбуватиметься через Doctor::addPatient(). // Ми визначимо цей метод після визначення класу Doctor, тому що нам спочатку потрібно визначити Doctor, щоб використовувати будь-що, пов'язане з ним void addDoctor(Doctor *doc); public: Patient(std::string name) : m_name(name) { } // Ми реалізуємо перевантаження оператора виводу нижче визначення класу Doctor, тому що він як раз і потрібен для реалізації перевантаження friend std::ostream& operator<<(std::ostream &out, const Patient &pat); std::string getName() const { return m_name; } // Ми робимо клас Doctor дружнім, щоб мати доступ до закритого методу addDoctor(). // Примітка: Ми б хотіли зробити дружнім тільки один метод addDoctor(), але ми не можемо це зробити, тому що Doctor попередньо оголошений friend class Doctor; }; class Doctor { private: std::string m_name; std::vector<Patient *> m_patient; public: Doctor(std::string name): m_name(name) { } void addPatient(Patient *pat) { // Лікар додає Пацієнта m_patient.push_back(pat); // Пацієнт додає Лікаря pat->addDoctor(this); } friend std::ostream& operator<<(std::ostream &out, const Doctor &doc) { unsigned int length = doc.m_patient.size(); if (length == 0) { out << doc.m_name << " has no patients right now"; return out; } out << doc.m_name << " is seeing patients: "; for (unsigned int count = 0; count < length; ++count) out << doc.m_patient[count]->getName() << ' '; return out; } std::string getName() const { return m_name; } }; void Patient::addDoctor(Doctor *doc) { m_doctor.push_back(doc); } std::ostream& operator<<(std::ostream &out, const Patient &pat) { unsigned int length = pat.m_doctor.size(); if (length == 0) { out << pat.getName() << " has no doctors right now"; return out; } out << pat.m_name << " is seeing doctors: "; for (unsigned int count = 0; count < length; ++count) out << pat.m_doctor[count]->getName() << ' '; return out; } int main() { // Створюємо Пацієнтів поза областю видимості класу Doctor Patient *p1 = new Patient("Anton"); Patient *p2 = new Patient("Ivan"); Patient *p3 = new Patient("Derek"); // Створюємо Лікарів поза областю видимості класу Patient Doctor *d1 = new Doctor("John"); Doctor *d2 = new Doctor("Tom"); d1->addPatient(p1); d2->addPatient(p1); d2->addPatient(p3); std::cout << *d1 << '\n'; std::cout << *d2 << '\n'; std::cout << *p1 << '\n'; std::cout << *p2 << '\n'; std::cout << *p3 << '\n'; delete p1; delete p2; delete p3; delete d1; delete d2; return 0; } |
Результат виконання програми:
John is seeing patients: Anton
Tom is seeing patients: Anton Derek
Anton is seeing doctors: John Tom
Ivan has no doctors right now
Derek is seeing doctors: Tom
Якщо говорити в загальному, то краще уникати двонаправлених асоціацій, якщо для вирішення завдання підходить і односпрямований зв’язок, так як двонаправлений зв’язок реалізувати складніше (з урахуванням виникнення можливих помилок) і він ускладнює логіку програми.
Рефлексивна асоціація
Іноді об’єкти можуть мати відносини з іншими об’єктами того ж типу. Це називається рефлексивною асоціацією. Хорошим прикладом рефлексивної асоціації є відношення між університетським курсом і його мінімальними вимогами для студентів.
Розглянемо спрощений випадок, коли Курс може мати тільки одну Вимогу:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <string> class Course { private: std::string m_name; Course *m_condition; public: Course(std::string &name, Course *condition=nullptr): m_name(name), m_condition(condition) { } }; |
Це може призвести до “ланцюжка” асоціацій (курс має необхідну умову, виконання якої включає ще одну умову і т.д.).
Асоціації можуть бути непрямими
У прикладах, наведених вище, ми використовували вказівники для зв’язування об’єктів. Однак в асоціації це не є обов’язковою умовою. Можна використовувати будь-які дані, які дозволяють зв’язати два об’єкти. У наступному прикладі ми покажемо, як клас Водій може мати односпрямований зв’язок з класом Автомобіль без змінної-члена у вигляді вказівника на об’єкт класу Автомобіль:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <iostream> #include <string> class Car { private: std::string m_name; int m_id; public: Car(std::string name, int id) : m_name(name), m_id(id) { } std::string getName() { return m_name; } int getId() { return m_id; } }; // Наш CarLot, по суті, є статичним масивом, який містить Автомобілі, і має функцію для "видачі" Автомобілів. // Оскільки масив є статичним, то нам не потрібно створювати об'єкти для використання класу CarLot class CarLot { private: static Car s_carLot[4]; public: CarLot() = delete; static Car* getCar(int id) { for (int count = 0; count < 4; ++count) if (s_carLot[count].getId() == id) return &(s_carLot[count]); return nullptr; } }; Car CarLot::s_carLot[4] = { Car("Camry", 5), Car("Focus", 14), Car("Vito", 73), Car("Levante", 58) }; class Driver { private: std::string m_name; int m_carId; // для зв'язування класів, замість вказівника, використовується Ідентифікатор (цілочисельне значення) public: Driver(std::string name, int carId) : m_name(name), m_carId(carId) { } std::string getName() { return m_name; } int getCarId() { return m_carId; } }; int main() { Driver d("Ivan", 14); // Ivan використовує машину з ID 14 Car *car = CarLot::getCar(d.getCarId()); // отримуємо цей Автомобіль з CarLot if (car) std::cout << d.getName() << " is driving a " << car->getName() << '\n'; else std::cout << d.getName() << " couldn't find his car\n"; return 0; } |
Результат виконання програми:
Ivan is driving a Focus
У прикладі, наведеному вище, у нас є CarLot (Гараж) в якому знаходяться наші автомобілі. Водій, якому потрібен Автомобіль, не має вказівника на цей Автомобіль — замість цього у нього є Ідентифікатор Автомобіля, який він може використати для отримання Автомобіля з Гаражу, коли йому це потрібно.
Саме в цьому прикладі реалізація виглядає дещо нерозумною, так як отримання Автомобіля з Гаражу вимагає додаткового виконання процесів (було б швидше, якби існував вказівник, який з’єднує безпосередньо два класи). Проте є і переваги прив’язки об’єктів до ідентифікатора замість використання вказівника. Наприклад, ви можете посилатися на об’єкти, які зараз не знаходяться в пам’яті (можливо, вони знаходяться в файлі або в базі даних і можуть бути завантажені на вимогу).
Композиція vs. Агрегація vs. Асоціація
Ось таблиця, яка допоможе вам швидко згадати відмінності між композицією, агрегацією і асоціацією:
| Властивості | Композиція | Агрегація | Асоціація |
| Відносини | Частин-Цілого | Частин-Цілого | Об’єкти не зв’язані між собою |
| Члени можуть належати відразу декільком класам | Ні | Так | Так |
| Існування членів керується класами | Так | Ні | Ні |
| Вид відносин | Односпрямовані | Односпрямовані | Односпрямовані або Двонаправлені |
| Тип відносин | “Частина чогось” | “Має” | “Використовує” |

(53 оцінок, середня: 4,89 з 5)
Привіт привіт
до глави “Реалізація асоціацій” і коментаря:
можливо з такою побудовою було б добре ? чи я помилився ? ^^