На предыдущих уроках мы рассмотрели два подтипа композиции объектов: композицию и агрегацию. Композиция объектов используется для моделирования отношений, в которых сложный объект (целое) состоит из нескольких более простых объектов (частей).
На этом уроке мы рассмотрим следующий тип отношений между двумя несвязанными объектами — ассоциацию. В отличие от композиции объектов, в ассоциации нет отношений «частей-целого».
Ассоциация
В ассоциации два несвязанных объекта должны соответствовать следующим отношениям:
Первый объект (член) не связан со вторым объектом (классом).
Первый объект (член) может принадлежать одновременно сразу нескольким объектам (классам).
Первый объект (член) существует, не управляемый вторым объектом (классом).
Первый объект (член) может знать или не знать о существовании второго объекта (класса).
В отличие от композиции или агрегации, где часть является частью целого, в ассоциации объекты между собой никак не связаны. Подобно агрегации, первый объект может принадлежать сразу нескольким объектам одновременно и не управляется ими. Однако, в отличие от агрегации, где отношения однонаправленные, в ассоциации отношения могут быть как однонаправленными, так и двунаправленными (когда оба объекта знают о существовании друг друга).
Отношения между врачами и пациентами — это отличный пример ассоциации. Врач связан с пациентом, но эти отношения нельзя назвать отношениями «части-целого». Врач может принимать десятки пациентов в день, а пациент может обращаться к нескольким врачам.
Мы можем сказать, что типом отношений в ассоциации является «использует». Врач «использует» пациента для получения дохода. Пациент «использует» врача, чтобы вылечить болезнь или улучшить свое самочувствие.
Реализация ассоциаций
Ассоциации реализованы по-разному. Однако чаще всего они реализованы через указатели, где классы указывают на объекты друг друга.
В следующем примере мы реализуем двунаправленную связь между Врачом и Пациентом, так как Врач должен знать своих Пациентов в лицо, а Пациенты могут обращаться к разным Врачам:
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. Ассоциация
Вот таблица, которая поможет вам быстро разобраться/вспомнить различия между композицией, агрегацией и ассоциацией:
Свойства | Композиция | Агрегация | Ассоциация |
Отношения | Части-целое | Части-целое | Объекты не связаны между собой |
Члены могут принадлежать одновременно сразу нескольким классам | Нет | Да | Да |
Существование членов управляется классами | Да | Нет | Нет |
Вид отношений | Однонаправленные | Однонаправленные | Однонаправленные или Двунаправленные |
Тип отношений | «Часть чего-то» | «Имеет» | «Использует» |
Зачем мы удаляем конструктор по умолчанию CarLot?
// Примечание: Мы бы хотели сделать дружественным только один метод addDoctor(), но мы не можем это сделать, так как Doctor предварительно объявлен
Мне не понятен этот комментарий из первого кода.
Во-первых, метод addDoctor() — приватный, сделав его friend классу Docrtor, мы получим доступ к приватным членам класса, но как мы его вызовем из класса Doctor, если он приватный ?
Так же, сделав метод addDoctor публичным, мы нарушим комментарий находящийся выше :
// Мы объявляем метод addDoctor() закрытым, так как не хотим его публичного использования.
Метод addDoctor, который использует адрес объекта типа Doctor, не является дружественным классу не из-за того, что класс Doctor объявлен предварительно, как указанно в комментарии.
Дублирую комментарий из первого кода еще раз :
// Примечание: Мы бы хотели сделать дружественным только один метод addDoctor(), но мы не можем это сделать, так как Doctor предварительно объявлен
В первом коде, кстати, совсем не обязательно выносить определение метода Patient::addDoctor за пределы класса. Это понадобилось бы, если бы в определении метода использовались переменные-члены класса Doctor или методы класса Doctor. А т.к. в определении мы используем только лишь сам объект doc, то компилятору не нужно будет заглядывать в определение класса Doctor, чтобы понять, что от него хотят. Проверьте, мой вариант тоже будет работать=)
Или если бы в методе была перегрузка оператора, то тоже нужно было бы выносить метод за пределы класса. Например, если бы было написано что-то вроде такого:
Компилятор в таком варианте ещё не знает про перегрузку
поэтому мы бы получили ошибку
Юрий, пожалуйста ответьте!!!Скажите как в рефлексивной ассоциации создать объект, в котором второй параметр НЕ будет указывать на nullptr??? То есть как сделать так, чтобы часть(переменная-член) класса Course указывала на объект не инициализированный нулем.На что должна указывать часть???Надеюсь вы меня поймете…
Здравствуйте.
Вы меня извините, перечитываю уже несколько раз и через некоторое время все равно не понимаю.
Что значит:
Почему не просто
Тарас,можете перечитать урок №124,но если вкратце,то для статических переменных-членов нужно предоставлять инициализатор(тип) для ЯВНОГО определения,так как переменная не принадлежит объектам класса ,а самому классу.
В примере предварительное объявление класса Doctor не требуется, если классы объявляются в разных файлах и инклюдятся, я прав? Т. е. на практике такое предварительное объявление не должно встречаться, так как плохой тон объявлять классы в одном файле.
Да. Ты прав. В принцепе так можно, но пример выше служит исключительно для наглядности.