En programación C++, se utilizan a menudo dos formas de combinar objetos: la agregación y la composición. Estos conceptos reflejan diferentes relaciones entre clases e impactan en el ciclo de vida y la responsabilidad de destruir objetos relacionados.
Historia de la cuestión:
En el diseño orientado a objetos, siempre ha sido importante separar las dependencias entre objetos. Con la aparición de lenguajes orientados a objetos (Smalltalk, C++, Java), surgió la cuestión de cómo modelar mejor las relaciones "parte - todo". En C++, esto se volvió especialmente relevante debido al manejo manual de la memoria y el ciclo de vida de los objetos.
Problema:
La elección errónea entre agregación y composición puede conducir a fugas de memoria, duplicación de recursos o errores en la destrucción de objetos. Además, estos conceptos a menudo se confunden.
Solución:
Ejemplo de código:
// Composición: class Engine {}; class Car { Engine engine; // Engine se crea y destruye junto con Car }; // Agregación: class Person {}; class Team { std::vector<Person*> members; // Apunta a objetos Person, no los posee };
Características clave:
Si un miembro de clase tiene un puntero a un objeto, ¿siempre es agregación?
¡No! Si la clase posee este puntero (por ejemplo, a través de std::unique_ptr), sigue siendo composición. El tipo de relación no se define por el tipo del campo, sino por la responsabilidad del ciclo de vida.
class House { std::unique_ptr<Room> room; // composición, House posee Room };
¿Puede la composición implementarse a través de una referencia o puntero crudo?
Puede, pero solo si el objeto es creado y destruido por el propietario, y la referencia o puntero se utiliza para optimización. Sin embargo, es mucho mejor usar objetos por valor o punteros inteligentes para expresar claramente la propiedad.
¿Qué sucede si en una composición un objeto-parte se crea fuera del propietario y se le pasa?
En ese caso, hay un riesgo de violación de los invariantes de la composición: si un objeto creado externamente se pasa al propietario y este lo destruye, y hay otra referencia a él en otro lugar, aparecerá un puntero colgante. Es necesario definir claramente los derechos de propiedad y la responsabilidad de destrucción en el proyecto.
Un equipo decidió almacenar todos los objetos anidados a través de punteros crudos en un contenedor y destruirlos manualmente en el destructor. Todo funcionó hasta que cambiaron el esquema de propiedad. Como resultado, el puntero se liberó dos veces, provocando un crash.
Ventajas:
Desventajas:
Otro equipo adoptó std::unique_ptr para todas las relaciones verdaderamente propietarias, y utilizó referencias no poseedoras solo como referencias temporales. Esto expresó claramente la arquitectura.
Ventajas:
Desventajas: