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:
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:
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.
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:
Wady:
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:
Wady: