23.3 — Agregacja

W poprzednim lekcja 23.2 -- Kompozycja zauważyliśmy, że kompozycja obiektu to proces tworzenia złożonych obiektów z prostszych. Mówiliśmy także o jednym typie kompozycji obiektów, zwanym kompozycją. W relacji kompozycji cały obiekt jest odpowiedzialny za istnienie części.

W tej lekcji przyjrzymy się drugiemu podtypowi kompozycji obiektu, zwanemu agregacją.

Agregacja

Aby zakwalifikować się jako agregacja, cały obiekt i jego części muszą pozostawać w następującej relacji:

  • Część (element) jest częścią obiektu (klasa)
  • Część (element) może (w razie potrzeby) należeć do więcej niż jednego obiektu (klasy) na raz
  • Część (element) nie jego istnieniem nie zarządza obiekt (klasa)
  • Część (element) nie wie o istnieniu obiektu (klasy)

Podobnie jak kompozycja, agregacja jest nadal relacją część-całość, gdzie części zawarte są w całości i jest to relacja jednokierunkowa. Jednak w przeciwieństwie do kompozycji części mogą należeć do więcej niż jednego obiektu na raz, a cały obiekt nie odpowiada za istnienie i żywotność części. Kiedy tworzona jest agregacja, nie jest ona odpowiedzialna za tworzenie części. Kiedy agregacja ulega zniszczeniu, nie jest ona odpowiedzialna za zniszczenie części.

Rozważmy na przykład relację pomiędzy osobą a jej adresem domowym. W tym przykładzie dla uproszczenia powiemy, że każda osoba ma adres. Jednak adres ten może należeć do więcej niż jednej osoby na raz: na przykład zarówno do Ciebie, jak i Twojego współlokatora lub bliskiej Ci osoby. Jednak adres ten nie jest zarządzany przez tę osobę — adres prawdopodobnie istniał, zanim dana osoba tam dotarła, i będzie istniał po jej odejściu. Co więcej, człowiek wie, pod jakim adresem mieszka, ale adresy nie wiedzą, jacy ludzie tam mieszkają. Jest to zatem związek zagregowany.

Alternatywnie rozważ samochód i silnik. Silnik samochodu jest częścią samochodu. I chociaż silnik należy do samochodu, może należeć również do innych rzeczy, na przykład do osoby, która jest właścicielem samochodu. Samochód nie ponosi odpowiedzialności za powstanie lub zniszczenie silnika. I chociaż samochód wie, że ma silnik (musi to zrobić, aby gdziekolwiek dotrzeć), silnik nie wie, że jest częścią samochodu.

Jeśli chodzi o modelowanie obiektów fizycznych, użycie terminu „zniszczone” może być nieco ryzykowne. Ktoś mógłby się spierać: „Gdyby meteor spadł z nieba i zmiażdżył samochód, czy wszystkie jego części nie uległyby zniszczeniu?” Oczywiście, że tak. Ale to wina meteorytu. Ważne jest to, że samochód nie jest odpowiedzialny za zniszczenie swoich części (ale może to zrobić siła zewnętrzna).

Można powiedzieć, że modele agregacji mają relacje „ma-a” (wydział ma nauczycieli, samochód ma silnik).

Podobnie jak w przypadku kompozycji, części agregacji mogą być pojedyncze lub multiplikatywne.

Implementacja agregacje

Ponieważ agregacje są podobne do kompozycji w tym sensie, że są relacjami część-całość, są implementowane niemal identycznie, a różnica między nimi jest głównie semantyczna. W kompozycji zazwyczaj dodajemy nasze części do kompozycji, używając normalnych zmiennych składowych (lub wskaźników, w przypadku których proces alokacji i dezalokacji jest obsługiwany przez klasę kompozycji).

W agregacji dodajemy również części jako zmienne składowe. Jednakże te zmienne składowe są zazwyczaj referencjami lub wskaźnikami używanymi do wskazywania obiektów, które zostały utworzone poza zakresem klasy. W rezultacie agregacja zwykle albo przyjmuje obiekty, na które ma wskazywać, jako parametry konstruktora, albo zaczyna się pusta, a podobiekty są dodawane później za pomocą funkcji dostępu lub operatorów.

Ponieważ te części istnieją poza zakresem klasy, gdy klasa zostanie zniszczona, wskaźnik lub zmienna składowa odniesienia zostaną zniszczone (ale nie usunięte). W związku z tym same części nadal będą istnieć.

Przyjrzyjmy się bardziej szczegółowo przykładowi nauczyciela i wydziału. W tym przykładzie dokonamy kilku uproszczeń: Po pierwsze, na wydziale będzie pracował tylko jeden nauczyciel. Po drugie, nauczyciel nie będzie wiedział, do jakiego działu należy.

#include <iostream>
#include <string>
#include <string_view>

class Teacher
{
private:
  std::string m_name{};

public:
  Teacher(std::string_view name)
      : m_name{ name }
  {
  }

  const std::string& getName() const { return m_name; }
};

class Department
{
private:
  const Teacher& m_teacher; // This dept holds only one teacher for simplicity, but it could hold many teachers

public:
  Department(const Teacher& teacher)
      : m_teacher{ teacher }
  {
  }
};

int main()
{
  // Create a teacher outside the scope of the Department
  Teacher bob{ "Bob" }; // create a teacher

  {
    // Create a department and use the constructor parameter to pass
    // the teacher to it.
    Department department{ bob };

  } // department goes out of scope here and is destroyed

  // bob still exists here, but the department doesn't

  std::cout << bob.getName() << " still exists!\n";

  return 0;
}

W tym przypadku bob jest tworzony niezależnie od department, a następnie przekazywany do konstruktora department. Kiedy department jest zniszczone, m_teacher odniesienie ulega zniszczeniu, ale sam nauczyciel nie ulega zniszczeniu, więc nadal istnieje, dopóki nie zostanie niezależnie zniszczony później w main().

Wybierz odpowiednią relację dla tego, co modelujesz

Chociaż w powyższym przykładzie może wydawać się trochę głupie, że Nauczyciele nie wiedzą, w jakim dziale pracują, może to być całkowicie w porządku w kontekście danego programu. Kiedy ustalasz, jaki rodzaj relacji wdrożyć, zastosuj najprostszą relację, która odpowiada Twoim potrzebom, a nie tę, która wydaje się najlepiej pasować w prawdziwym kontekście.

Na przykład, jeśli piszesz symulator warsztatu blacharskiego, możesz chcieć zaimplementować samochód i silnik jako sumę, aby silnik można było wyjąć i odłożyć na półkę na później. Jeśli jednak piszesz symulację wyścigów, możesz chcieć zaimplementować samochód i silnik jako kompozycję, ponieważ w tym kontekście silnik nigdy nie będzie istniał poza samochodem.

Najlepsza praktyka

Zaimplementuj najprostszy typ relacji, który spełnia potrzeby Twojego programu, a nie to, co wydaje się właściwe w prawdziwym życiu.

Podsumowanie kompozycji i agregacja

Kompozycja:

  • Zazwyczaj używaj normalnych zmiennych składowych
  • Może używać wskaźników, jeśli klasa sama obsługuje alokację/delokację obiektów
  • Odpowiedzialny za tworzenie/zniszczenie części

Agregacje:

  • Zazwyczaj używaj wskaźników lub elementów referencyjnych, które wskazują do obiektów znajdujących się poza zakresem klasy agregatu lub odwoływania się do nich
  • Nie odpowiada za tworzenie/niszczenie części

Warto zauważyć, że koncepcje kompozycji i agregacji można swobodnie mieszać w ramach tej samej klasy. Całkiem możliwe jest napisanie klasy odpowiedzialnej za tworzenie/zniszczenie niektórych części, ale nie innych. Na przykład nasza klasa Działu może mieć imię i Nauczyciela. Nazwa prawdopodobnie zostałaby dodana do Departamentu w drodze składu oraz zostałaby utworzona i zniszczona wraz z Departamentem. Z drugiej strony Nauczyciel zostałby dodany do działu poprzez agregację i niezależnie utworzony/zniszczony.

Chociaż agregacje mogą być niezwykle przydatne, są również potencjalnie bardziej niebezpieczne, ponieważ agregacje nie radzą sobie z dezalokacją swoich części. Dealokację pozostawia się stronie zewnętrznej. Jeśli strona zewnętrzna nie ma już wskaźnika ani odniesienia do porzuconych części, lub jeśli po prostu zapomni oczyścić (zakładając, że klasa sobie z tym poradzi), nastąpi wyciek pamięci.

Z tego powodu kompozycje powinny być preferowane zamiast agregacji.

Kilka ostrzeżeń/errata

Z różnych powodów historycznych i kontekstowych, w przeciwieństwie do kompozycji, definicja agregacji nie jest precyzyjna — dlatego możesz zobaczyć, że inne materiały referencyjne definiują ją inaczej niż my. W porządku, po prostu bądź tego świadomy.

I ostatnia uwaga: w lekcji 13.7 — Wprowadzenie do struktur, elementów i wyboru elementów zdefiniowaliśmy zagregowane typy danych (takie jak struktury i klasy) jako typy danych, które grupują razem wiele zmiennych. W swoich podróżach po C++ możesz także natknąć się na termin klasa zagregowana , który jest zdefiniowany jako struktura lub klasa, która nie ma dostarczonych konstruktorów, destruktorów ani przeciążonych przypisań, ma wszystkie publiczne elementy członkowskie i nie korzysta z dziedziczenia — zasadniczo jest to zwykła struktura starych danych. Pomimo podobieństw w nazewnictwie, agregaty i agregacja są różne i nie należy ich mylić.

std::reference_wrapper

W Department/Teacher przykładzie powyżej użyliśmy odniesienia w Department do przechowywania Teacher. Działa to dobrze, jeśli jest tylko jeden Teacher, ale co, jeśli wydział ma wielu nauczycieli? Chcielibyśmy przechowywać tych Nauczycieli na jakiejś liście (np. std::vector), ale stałe tablice i różne standardowe listy bibliotek nie mogą przechowywać odniesień (ponieważ elementy listy muszą być przypisywalne, a odniesień nie można ponownie przypisać).

std::vector<const Teacher&> m_teachers{}; // Illegal

Zamiast odniesień moglibyśmy używać wskaźników, ale to otworzyłoby możliwość przechowywania lub przekazywania wskaźników zerowych. W przykładzie Department/Teacher nie chcemy zezwalać na wskaźniki zerowe. Aby rozwiązać ten problem, istnieje std::reference_wrapper.

Zasadniczo std::reference_wrapper to klasa, która działa jak referencja, ale umożliwia także przypisywanie i kopiowanie, więc jest kompatybilna z listami takimi jak std::vector.

Dobra wiadomość jest taka, że ​​tak naprawdę nie musisz rozumieć, jak to działa, aby z niej korzystać. Wszystko, co musisz wiedzieć, to trzy rzeczy:

  1. std::reference_wrapper znajduje się w nagłówku <funkcjonalny>.
  2. Kiedy tworzysz std::reference_wrapper obiekt opakowany, obiekt nie może być obiektem anonimowym (ponieważ obiekty anonimowe mają zakres wyrażenia, a to pozostawiłoby odwołanie w zawieszeniu).
  3. Jeśli chcesz odzyskać obiekt z std::reference_wrapper, używasz get() funkcja składowa.

Oto przykład użycia std::reference_wrapper w std::vector:

#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>
#include <string>

int main()
{
  std::string tom{ "Tom" };
  std::string berta{ "Berta" };

  std::vector<std::reference_wrapper<std::string>> names{ tom, berta }; // these strings are stored by reference, not value

  std::string jim{ "Jim" };

  names.emplace_back(jim);

  for (auto name : names)
  {
    // Use the get() member function to get the referenced string.
    name.get() += " Beam";
  }

  std::cout << jim << '\n'; // prints Jim Beam

  return 0;
}

Aby utworzyć wektor stałych odniesień, musielibyśmy dodać const przed std::string w ten sposób

// Vector of const references to std::string
std::vector<std::reference_wrapper<const std::string>> names{ tom, berta };

Czas quizu

Pytanie nr 1

Czy zrobiłbyś to czy jest bardziej prawdopodobne, że zastosuje poniższe elementy jako kompozycję lub połączenie?
a) Kolorowa piłka
b) Pracodawca zatrudniający wiele osób
c) Wydziały na uniwersytecie
d) Twój wiek
e) Worek kulek

Pokaż rozwiązanie

Pytanie nr 2

Zaktualizuj Department/Teacher przykład tak, aby Department mogł obsłużyć wielu Nauczycieli. Powinien zostać wykonany następujący kod:

#include <iostream>

// ...

int main()
{
  // Create a teacher outside the scope of the Department
  Teacher t1{ "Bob" };
  Teacher t2{ "Frank" };
  Teacher t3{ "Beth" };

  {
    // Create a department and add some Teachers to it
    Department department{}; // create an empty Department

    department.add(t1);
    department.add(t2);
    department.add(t3);

    std::cout << department;

  } // department goes out of scope here and is destroyed

  std::cout << t1.getName() << " still exists!\n";
  std::cout << t2.getName() << " still exists!\n";
  std::cout << t3.getName() << " still exists!\n";

  return 0;
}

To powinno zostać wydrukowane:

Department: Bob Frank Beth
Bob still exists!
Frank still exists!
Beth still exists!

Pokaż wskazówkę

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
306 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze