16.4 – Ассоциация

Добавлено 26 июля 2021 в 13:14

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

В этом уроке мы рассмотрим более слабый тип связи между двумя иначе не связанными объектами, называемый ассоциацией. В отличие от связей композиции объектов, в ассоциации нет подразумеваемой связи целое/часть.

Ассоциация

Чтобы считаться ассоциацией, объект и другой объект должны иметь следующие связи:

  • связанный объект (член) в противном случае не связан с объектом (классом);
  • связанный объект (член) может принадлежать более чем одному объекту (классу) одновременно;
  • существование связанного объекта (члена) не управляется объектом (классом);
  • связанный объект (член) может знать или не знать о существовании объекта (класса).

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

Отличный пример ассоциации – связи между врачами и пациентами. У доктора явно есть связь со своими пациентами, но концептуально это не связь «часть-целое» (композиция объектов). Врач может осмотреть множество пациентов за день, а пациент может обратиться к множеству врачей (возможно, он хочет получить второе мнение или посещает разные типы врачей). Продолжительность жизни одного объекта не привязана к другому.

Можно сказать, что ассоциация моделирует связь «использует». Врач «использует» пациента (для заработка). Пациент «использует» врача (в любых медицинских целях).

Реализация ассоциаций

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

В этом примере мы реализуем двустороннюю связь доктор/пациент (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-битного целого числа может сэкономить много памяти.

Резюме: композиция, агрегация, ассоциация

Ниже приведена сводная таблица, которая поможет вам запомнить разницу между композицией, агрегацией и ассоциацией:

Сводные данные о композиции, агрегации и ассоциации
СвойствоКомпозицияАгрегацияАссоциация
Тип связицелое/частьцелое/частьв противном случае не связаны
Члены могут принадлежать нескольким объектам классанетдада
Существование членов управляется классомданетнет
Направленностьоднонаправленнаяоднонаправленнаяоднонаправленная или двунаправленная
Выражение связичасть чего-тоимеет что-тоиспользует что-то

Теги

C++ / CppLearnCppДля начинающихОбучениеОбъектно-ориентированное программирование (ООП)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.