programowanieArchitekt oprogramowania C++

Co to jest agregacja (aggregation) i kompozycja (composition) w C ++? Jak się od siebie różnią i kiedy używać którego podejścia?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

W programowaniu w C++ często wykorzystuje się dwa sposoby łączenia obiektów: agregację i kompozycję. Te koncepcje odzwierciedlają różne relacje pomiędzy klasami i wpływają na cykl życia oraz odpowiedzialność za niszczenie powiązanych obiektów.

Historia pytania:

W projektowaniu obiektowym zawsze ważne było oddzielanie zależności między obiektami. Wraz z pojawieniem się języków obiektowych (Smalltalk, C++, Java) pojawiło się pytanie: jak najlepiej modelować relacje "część – całość". W C++ stało się to szczególnie istotne z powodu ręcznego zarządzania pamięcią i cyklem życia obiektów.

Problem:

Błędny wybór między agregacją a kompozycją prowadzi albo do wycieków pamięci, albo do duplikacji zasobów, albo do błędów w niszczeniu obiektów. Często myli się także te pojęcia.

Rozwiązanie:

  • Kompozycja — to relacja, w której obiekt posiada część-obiekt i odpowiada za jego tworzenie/niszczenie. W C++ zazwyczaj wyraża się to jako człon-klasa przez wartość lub za pomocą unique_ptr.
  • Agregacja — to słabsze połączenie, część-obiekt istnieje poza "całością" i odpowiedzialność za jego cykl życia nie leży na właścicielu. Zazwyczaj realizowana jest poprzez odwołanie (wskaźnik/referencję) bez własności.

Przykład kodu:

// Kompozycja: class Silnik {}; class Samochód { Silnik silnik; // Silnik tworzony i niszczony razem z Samochodem }; // Agregacja: class Osoba {}; class Zespół { std::vector<Osoba*> członkowie; // Wskazuje na obiekty Osoba, nie posiada ich };

Kluczowe cechy:

  • Kompozycja — silne połączenie (część z całością), posiada
  • Agregacja — słabe połączenie (używa), nie posiada
  • Kompozycja automatyzuje zarządzanie pamięcią, agregacja wymaga ostrożności i umów dotyczących właścicieli

Pytania z podstępem.

Czy jeśli w członie klasy leży wskaźnik na obiekt, to zawsze jest to agregacja?

Nie! Jeśli klasa posiada ten wskaźnik (na przykład przez std::unique_ptr), nadal jest to kompozycja. Typ połączenia określa się nie przez typ pola, lecz przez odpowiedzialność za cykl życia.

class Dom { std::unique_ptr<Pokój> pokój; // kompozycja, Dom posiada Pokój };

Czy kompozycja może być realizowana przez odniesienie lub surowy wskaźnik?

Może — ale tylko jeśli obiekt jest tworzony i niszczony przez właściciela, a odniesienie lub wskaźnik jest używane do optymalizacji. Znacznie lepiej jest jednak używać obiektów przez wartość lub wskaźników inteligentnych w celu wyraźnego wyrażenia własności.

Co się stanie, jeśli w kompozycji obiekt-część zostanie utworzony poza właścicielem i mu przekazany?

W takim przypadku istnieje ryzyko naruszenia inwariantów kompozycji: jeśli obiekt utworzony zewnętrznie zostanie przekazany właścicielowi, a on go niszczy, podczas gdy gdzie indziej pozostało na niego odniesienie — pojawi się dangling pointer. Należy ściśle określić prawa własności i odpowiedzialność za niszczenie w projekcie.

Typowe błędy i antywzorce

  • Mieszanie pojęć agregacji i kompozycji (na przykład przechowywanie niepotrzebnych surowych wskaźników, ale próba ich niszczenia w destruktorze właściciela)
  • Używanie agregacji tam, gdzie wymagana jest ścisła kontrola cyklu życia (na przykład detale złożonego obiektu)
  • Niezwalnianie wskaźników nieposiadających

Przykład z życia

Negatywny przypadek

Jedna drużyna postanowiła przechowywać wszystkie obiekty wewnętrzne przez surowe wskaźniki w kontenerze i ręcznie je niszczyć w destruktorze. Wszystko działało, dopóki nie zmieniono schematu własności. W rezultacie wskaźnik został zwolniony dwukrotnie, co spowodowało awarię.

Zalety:

  • Elastyczność architektury dla niektórych wariantów (na przykład pływających relacji między obiektami)

Wady:

  • Wysokie ryzyko błędów w zarządzaniu pamięcią
  • Trudne do utrzymania

Pozytywny przypadek

Inna drużyna przeszła na std::unique_ptr dla wszystkich rzeczywistych połączeń właścicielskich, a nie-posiadające wykorzystywała tylko w postaci tymczasowych odniesień. To wyraźnie wyraziło architekturę.

Zalety:

  • Przezroczyste i zrozumiałe relacje własności
  • Brak wycieków lub podwójnych zwolnień

Wady:

  • Nie zawsze możliwa jest cykliczna kompozycja
  • Czasami trzeba dostosować protokoły komunikacji między obiektami