2. Związki pomiędzy obiektami

2.2. Asocjacja

Asocjacje są silniejszymi relacjami niż zależności. Wskazują, że jeden obiekt jest związany z innym przez określony czas. Jednak czas życia obu obiektów nie jest od siebie zależny, usunięcie jednego nie powoduje usunięcia drugiego. Nie można też powiedzieć, że jeden obiekt jest częścią drugiego. 

W języku polskim asocjacje zazwyczaj opisuje się słowami posiada, należy do, itp ... lecz uważajcie by ich nie mylić z agregacjami. W relacji asocjacji żaden z obiektów nie jest właścicielem drugiego, nie zarządza jego czasem życia, także samo zerwanie powiązania nie wpływa na czas życia obu obiektów. Potraktujcie ich jako partnerów - w ziązku asocjaci są ludzie którzy występują w jednej drużynie lub zapisali się do tego samego stowarzyszenia. Obiekty powiązane asocjacją mogą posiadać wzajemne referencje, jeden może się odwołać do drugiego, etc.

Z formalnego punktu widzenia, obiekty będące w związku asocjacji charakteryzują następujące cechy: 

  • Powiązany obiekt  może istnieć bez związku z drugim obiektem
  • Powiązany obiekt  może należeć do więcej niż jednego obiektu jednocześnie 
  • Czasem życia powiązanego obiektu nie zarządza drugi obiekt z którym pozostaje on w relacji asocjacji. 
  • Powiązany obiekt może  ale nie musi wiedzieć o istnieniu obiektu - związek może być jedno-, lub dwukierunkowy. 
Popatrzmy na relację między studentami a wykładowcami jako na przykład asocjacji. Wykładowca ma wyraźny związek ze swoimi studentami, ale ani student nie jest częścią wykładowcy, ani wykładowca nie jest częścią studenta. Wykładowca może mieć zajęcia z wieloma studentami, a student może uczestniczyć w zajęciach różnych wykładowców. 

 W języku polskim asocjację oddaje się słowami "należy do",  "używa", czasem także "posiada" - ale nie w rozumieniu omówionej później agregacji. W przypadku wykładowcy i studenta - wykładowca "używa" studentów w celu realizacji swojej pracy (niesie kaganek oświaty). Student "używa" wykładowcy do zdobycia wiedzy. 

Ponieważ asocjacje są szerokim typem relacji, mogą być implementowane na wiele różnych sposobów. Jednak jak chcecie jasno wyrazić swoją intencję - w C++ najlepiej implementować asocjacje użyciu wskaźników, gdzie obiekt wskazuje na powiązany obiekt.

W tym przykładzie zaimplementujemy relację dwukierunkową student/wykładowca - ma to sens jeśli wiecie kto Was jako studentów uczy, i ma też sens jeśli uczący wie kogo uczy ... ;)  

Jako ciekawostki - zamiast czystych wskaźników (których stosowanie jest nieeleganckie) w poniższym przykładzie użyliśmy klasy reference_wrapper - miłego dodatku z STL który pozwala nam łatwo kopiować i przypisywać referencje, co w efekcie pozwala na przechowywanie ich w wektorze. Oczywiście równie dobrze można użyć klasy std::shared_ptr,  w połączeniu z std::weak_ptr. 


#include <iostream>

#include <functional>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

using namespace std::literals;

// ponieważ mamy zależność dwustronną - potrzebna nam jest wstępna deklaracja
// celowo nie wprowadzamy dziedziczenia i duplikujemy część pól w klasach
// by skupić się na asocjacji.
class CStudent;

class CWykladowca
{
public:
    CWykladowca(std::string_view imie, std::string_view nazwisko) : m_imie{imie}, m_nazwisko{nazwisko} {  }

    void dodajStudenta(CStudent& student);

    friend std::ostream& operator<<(std::ostream& out, const CWykladowca& w);

    std::string nazwa() const { return m_imie+" "s+m_nazwisko; }

private:
    std::string m_imie, m_nazwisko;
    std::vector<std::reference_wrapper<const CStudent>> m_studenci;
};

class CStudent
{
public:
    CStudent(std::string_view imie, std::string_view nazwisko) : m_imie{imie}, m_nazwisko{nazwisko} {  }

    friend std::ostream& operator<<(std::ostream& out, const CStudent& s);

    std::string nazwa() const { return m_imie+" "s+m_nazwisko; }

    // Metoda jest zaprzyjaźniona by mogła wywołać dodajWykladowce
    friend void CWykladowca::dodajStudenta(CStudent &student);

private:
    std::string m_imie, m_nazwisko;
    std::vector<std::reference_wrapper<const CWykladowca>> m_wykladowcy;

    // Mimo iż związek jest dwustronny - celowo ukryliśmy możliwość dodawania wykładowców u studentów,
    // wyuszając by związek był nawiązywany zawsze przez CWykladowca::dodajStudenta
    void dodajWykladowce(const CWykladowca& w)
    {
        m_wykladowcy.push_back(w);
    }

};

void CWykladowca::dodajStudenta(CStudent &student)
{
    m_studenci.push_back(student);
    student.dodajWykladowce(*this);
}

std::ostream& operator<<(std::ostream& out, const CWykladowca& w)
{
    if (w.m_studenci.empty()) {
        out << w.nazwa() << " nie ma jeszcze studentów";
        return out;
    }

    out << w.nazwa() << " wykłada dla: ";
    for (const auto& student : w.m_studenci)
        out << student.get().nazwa() << ' ';

    return out;
}

std::ostream& operator<<(std::ostream& out, const CStudent& student)
{
    if (student.m_wykladowcy.empty()) {
        out << student.nazwa() << " do nikogo nie uczęszcza";
        return out;
    }

    out << student.nazwa() << " uczęszcza na wykłady do: ";
    for (const auto& wykladowca : student.m_wykladowcy)
        out << wykladowca.get().nazwa() << ' ';

    return out;
}

int main()
{
    // Studenci powstają niezależnie od wykładowców, i w dowolnej kolejności
    CStudent janek{ "Janek", "Konieczny" };
    CStudent basia{ "Basia", "Niespodziana" };
    CStudent stasio{ "Stasio", "Przypadkowy" };

    CWykladowca mikolaj{ "Mikołaj", "Kopernik" };
    CWykladowca maria{ "Maria", "Skłodowska-Curie" };

    CStudent zosia{ "Zosia", "Drążąca" };

    mikolaj.dodajStudenta(janek);

    maria.dodajStudenta(janek);
    maria.dodajStudenta(basia);
    maria.dodajStudenta(zosia);

    std::cout << "Wykładowcy:\n";
    std::cout << mikolaj << '\n';
    std::cout << maria << '\n';
    std::cout << "Studenci:\n";
    std::cout << janek << '\n';
    std::cout << basia << '\n';
    std::cout << stasio << '\n';
    std::cout << zosia << '\n';

    return 0;
}