16.4 – Ассоциация
В предыдущих двух уроках мы рассмотрели два типа композиции объектов: композицию и агрегацию. Композиция объектов используется для моделирования связей, когда сложный объект построен из одного или нескольких более простых объектов (частей).
В этом уроке мы рассмотрим более слабый тип связи между двумя иначе не связанными объектами, называемый ассоциацией. В отличие от связей композиции объектов, в ассоциации нет подразумеваемой связи целое/часть.
Ассоциация
Чтобы считаться ассоциацией, объект и другой объект должны иметь следующие связи:
- связанный объект (член) в противном случае не связан с объектом (классом);
- связанный объект (член) может принадлежать более чем одному объекту (классу) одновременно;
- существование связанного объекта (члена) не управляется объектом (классом);
- связанный объект (член) может знать или не знать о существовании объекта (класса).
В отличие от композиции или агрегации, где часть является частью родительского объекта, в ассоциации связанный объект никак не связан с объектом. Как и в агрегации, связанный объект может принадлежать нескольким объектам одновременно, и он не управляется этими объектами. Однако, в отличие от агрегации, где связи всегда однонаправленные, в ассоциации связи могут быть однонаправленными или двунаправленными (когда два объекта знают друг о друге).
Отличный пример ассоциации – связи между врачами и пациентами. У доктора явно есть связь со своими пациентами, но концептуально это не связь «часть-целое» (композиция объектов). Врач может осмотреть множество пациентов за день, а пациент может обратиться к множеству врачей (возможно, он хочет получить второе мнение или посещает разные типы врачей). Продолжительность жизни одного объекта не привязана к другому.
Можно сказать, что ассоциация моделирует связь «использует». Врач «использует» пациента (для заработка). Пациент «использует» врача (в любых медицинских целях).
Реализация ассоциаций
Поскольку ассоциации – это широкий тип связей, они могут быть реализованы множеством различных способов. Однако чаще всего ассоциации реализуются с помощью указателей, где объект указывает на связанный объект.
В этом примере мы реализуем двустороннюю связь доктор/пациент (Doctor
/Patient
), поскольку докторам имеет смысл знать, кто их пациенты, и наоборот.
#include <functional> // reference_wrapper
#include <iostream>
#include <string>
#include <vector>
// Поскольку Doctor и Patient имеют циклическую зависимость,
// выполним предварительное объявление класса Patient
class Patient;
class Doctor
{
private:
std::string m_name{};
std::vector<std::reference_wrapper<const Patient>> m_patient{};
public:
Doctor(const std::string& name) :
m_name{ name }
{
}
void addPatient(Patient& patient);
// Мы реализуем эту функцию ниже определения Patient,
// так как нам нужно, чтобы Patient был определен для этой функции
friend std::ostream& operator<<(std::ostream &out, const Doctor &doctor);
const std::string& getName() const { return m_name; }
};
class Patient
{
private:
std::string m_name{};
std::vector<std::reference_wrapper<const Doctor>> m_doctor{};
// Мы собираемся сделать addDoctor закрытой, потому что не хотим,
// чтобы пользователи класса использовали ее.
// Вместо этого они должны использовать Doctor::addPatient(),
// которая открыта
void addDoctor(const Doctor& doctor)
{
m_doctor.push_back(doctor);
}
public:
Patient(const std::string& name)
: m_name{ name }
{
}
// Мы реализуем эту функцию ниже определения Doctor,
// так как нам нужно, чтобы Doctor был здесь определен
friend std::ostream& operator<<(std::ostream &out, const Patient &patient);
const std::string& getName() const { return m_name; }
// Мы сделаем Doctor::addPatient() другом, чтобы она могла
// получить доступ к закрытой функции Patient::addDoctor()
friend void Doctor::addPatient(Patient& patient);
};
void Doctor::addPatient(Patient& patient)
{
// Наш врач добавит этого пациента
m_patient.push_back(patient);
// и пациент также добавит этого доктора
patient.addDoctor(*this);
}
std::ostream& operator<<(std::ostream &out, const Doctor &doctor)
{
if (doctor.m_patient.empty())
{
out << doctor.m_name << " has no patients right now";
return out;
}
out << doctor.m_name << " is seeing patients: ";
for (const auto& patient : doctor.m_patient)
out << patient.get().getName() << ' ';
return out;
}
std::ostream& operator<<(std::ostream &out, const Patient &patient)
{
if (patient.m_doctor.empty())
{
out << patient.getName() << " has no doctors right now";
return out;
}
out << patient.m_name << " is seeing doctors: ";
for (const auto& doctor : patient.m_doctor)
out << doctor.get().getName() << ' ';
return out;
}
int main()
{
// Создаем пациентов вне области действия класса Doctor
Patient dave{ "Dave" };
Patient frank{ "Frank" };
Patient betsy{ "Betsy" };
Doctor james{ "James" };
Doctor scott{ "Scott" };
james.addPatient(dave);
scott.addPatient(dave);
scott.addPatient(betsy);
std::cout << james << '\n';
std::cout << scott << '\n';
std::cout << dave << '\n';
std::cout << frank << '\n';
std::cout << betsy << '\n';
return 0;
}
Эта программа печатает:
James is seeing patients: Dave
Scott is seeing patients: Dave Betsy
Dave is seeing doctors: James Scott
Frank has no doctors right now
Betsy is seeing doctors: Scott
В общем, вам следует избегать двунаправленных ассоциаций, если подходит однонаправленная, поскольку они добавляют сложность и, как правило, более подвержены ошибкам.
Рефлексивная ассоциация
Иногда объекты могут иметь связи с другими объектами того же типа. Это называется рефлексивной ассоциацией. Хорошим примером рефлексивной ассоциации является связь между университетским курсом и его предварительными требованиями (которые также являются университетскими курсами).
Рассмотрим упрощенный случай, когда у курса может быть только одно предварительное условие. Мы можем сделать что-то вроде этого:
#include <string>
class Course
{
private:
std::string m_name;
const Course *m_prerequisite;
public:
Course(const std::string &name, const Course *prerequisite = nullptr):
m_name{ name }, m_prerequisite{ prerequisite }
{
}
};
Это может привести к цепочке ассоциаций (у курса есть предварительные условия, у которых тоже есть предварительные условия и т.д.)
Ассоциации могут быть косвенными
Во всех предыдущих случаях мы использовали либо указатели, либо ссылки, чтобы напрямую связывать объекты вместе. Однако в ассоциации это не обязательно. Достаточно любых данных, которые позволяют связать два объекта вместе. В следующем примере мы показываем, как класс Driver
(водитель) может иметь однонаправленную ассоциацию с классом Car
(автомобиль) без фактического включения указателя или члена-ссылки на Car
:
#include <iostream>
#include <string>
class Car
{
private:
std::string m_name;
int m_id;
public:
Car(const std::string& name, int id)
: m_name{ name }, m_id{ id }
{
}
const std::string& getName() const { return m_name; }
int getId() const { return m_id; }
};
// Наш CarLot - это, по сути, просто статический массив из Car
// и функция поиска для их получения.
// Поскольку он статический, нам не нужно создавать объект
// типа CarLot для его использования
class CarLot
{
private:
static Car s_carLot[4];
public:
CarLot() = delete; // Убедимся, что мы не попытаемся создать CarLot
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]{ { "Prius", 4 }, { "Corolla", 17 }, { "Accord", 84 }, { "Matrix", 62 } };
class Driver
{
private:
std::string m_name;
int m_carId; // мы связаны с Car по ID, а не по указателю
public:
Driver(const std::string& name, int carId)
: m_name{ name }, m_carId{ carId }
{
}
const std::string& getName() const { return m_name; }
int getCarId() const { return m_carId; }
};
int main()
{
Driver d{ "Franz", 17 }; // Франц ведет машину с ID 17
Car *car{ CarLot::getCar(d.getCarId()) }; // Достаем машину со стоянки
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;
}
В приведенном выше примере у нас есть CarLot
, на котором находятся наши автомобили. У объекта Driver
, которому нужен автомобиль, нет указателя на его объект Car
– вместо этого у него есть ID автомобиля, который мы можем использовать, чтобы получить объект Car
из CarLot
, когда он нам понадобится.
В этом конкретном примере поступать таким образом довольно глупо, поскольку для извлечения объекта Car
из CarLot
требуется неэффективный поиск (указатель, соединяющий два объекта, намного быстрее). Однако у ссылки на объекты по уникальному идентификатору вместо указателя есть и преимущества. Например, вы можете ссылаться на вещи, которых в данный момент нет в памяти (возможно, они находятся в файле или в базе данных и могут быть загружены по запросу). Кроме того, указатели могут занимать 4 или 8 байтов – если пространство ограничено, а количество уникальных объектов довольно мало, обращение к ним с помощью 8-битного или 16-битного целого числа может сэкономить много памяти.
Резюме: композиция, агрегация, ассоциация
Ниже приведена сводная таблица, которая поможет вам запомнить разницу между композицией, агрегацией и ассоциацией:
Свойство | Композиция | Агрегация | Ассоциация |
---|---|---|---|
Тип связи | целое/часть | целое/часть | в противном случае не связаны |
Члены могут принадлежать нескольким объектам класса | нет | да | да |
Существование членов управляется классом | да | нет | нет |
Направленность | однонаправленная | однонаправленная | однонаправленная или двунаправленная |
Выражение связи | часть чего-то | имеет что-то | использует что-то |