In der Programmierung mit C++ gibt es oft zwei Möglichkeiten, Objekte zu kombinieren: Aggregation und Komposition. Diese Konzepte spiegeln unterschiedliche Beziehungen zwischen Klassen wider und beeinflussen den Lebenszyklus sowie die Verantwortung für die Zerstörung der verknüpften Objekte.
Hintergrund:
Im objektorientierten Design war es schon immer wichtig, Abhängigkeiten zwischen Objekten zu trennen. Mit dem Aufkommen von objektorientierten Sprachen (Smalltalk, C++, Java) stellte sich die Frage, wie man am besten Beziehungen zwischen "Teil und Ganzem" modellieren kann. In C++ wurde dies besonders relevant aufgrund der manuellen Speicherverwaltung und des Lebenszyklus von Objekten.
Problem:
Eine falsche Wahl zwischen Aggregation und Komposition führt entweder zu Speicherlecks, zu redundanten Ressourcen oder zu Objektzerstörungsfehlern. Außerdem werden diese Konzepte oft verwechselt.
Lösung:
Beispielcode:
// Komposition: class Engine {}; class Car { Engine engine; // Engine wird zusammen mit Car erstellt und zerstört }; // Aggregation: class Person {}; class Team { std::vector<Person*> members; // Zeigt auf Person-Objekte, besitzt sie nicht };
Wesentliche Merkmale:
Wenn in einem Mitgliedsklasse ein Zeiger auf ein Objekt liegt, ist das immer Aggregation?
Nein! Wenn die Klasse diesen Zeiger besitzt (zum Beispiel über std::unique_ptr), handelt es sich dennoch um Komposition. Der Typ der Beziehung wird nicht durch den Typ des Feldes bestimmt, sondern durch die Verantwortung für den Lebenszyklus.
class House { std::unique_ptr<Room> room; // Komposition, House besitzt Room };
Kann Komposition auch über Referenzen oder rohe Zeiger realisiert werden?
Ja, aber nur wenn das Objekt vom Besitzer erstellt und zerstört wird und die Referenz oder der Zeiger zur Optimierung verwendet wird. Es ist jedoch viel besser, Werte oder intelligente Zeiger zu verwenden, um das Eigentum klar auszudrücken.
Was passiert, wenn in einer Komposition das Teilobjekt außerhalb des Besitzers erstellt und an diesen übergeben wird?
In diesem Fall besteht das Risiko, die Invarianten der Komposition zu verletzen: Wenn das extern erstellte Objekt an den Besitzer übergeben und von diesem zerstört wird, während irgendwo anders noch ein Zeiger auf dieses Objekt existiert, entsteht ein Dangling Pointer. Es ist wichtig, die Eigentumsrechte und Verantwortlichkeiten für die Zerstörung im Projekt klar zu definieren.
Ein Team entschied sich, alle eingebetteten Objekte über rohe Zeiger in einem Container zu speichern und manuell im Destruktor zu zerstören. Alles funktionierte, bis sich das Eigentumsschema änderte. Infolgedessen wurde der Zeiger doppelt freigegeben, was zu einem Absturz führte.
Vorteile:
Nachteile:
Ein anderes Team wechselte zu std::unique_ptr für alle tatsächlich besitzenden Verbindungen und verwendete nicht besitzende nur als temporäre Referenzen. Dies drückte die Architektur klar aus.
Vorteile:
Nachteile: