ProgramaciónArquitecto de Software en C++

¿Qué son la agregación (aggregation) y la composición (composition) en C++? ¿Cómo se diferencian entre sí y cuándo usar cada enfoque?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

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:

  • Composición: es una relación en la que un objeto posee a otro objeto-parte y es responsable de su creación/destrozo. En C++, esto se expresa generalmente a través de un miembro de clase por valor o mediante unique_ptr.
  • Agregación: es una relación más débil, el objeto-parte existe fuera del "todo", y la responsabilidad de su ciclo de vida no recae en el propietario. Generalmente se implementa a través de una referencia (puntero/reference) no poseedora.

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:

  • Composición: relación fuerte (parte de), posee
  • Agregación: relación débil (usa), no posee
  • La composición automatiza la gestión de memoria, la agregación requiere precaución y acuerdos sobre propietarios

Preguntas engañosas.

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.

Errores comunes y anti-patrones

  • Mezclar los conceptos de agregación y composición (por ejemplo, almacenar punteros crudos innecesarios, pero intentar destruirlos en el destructor del propietario)
  • Usar agregación donde se necesita un ciclo de vida estricto (por ejemplo, partes de un objeto complejo)
  • No liberar punteros no poseedores

Ejemplo de la vida real

Caso negativo

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:

  • Flexibilidad arquitectónica para algunas variantes (por ejemplo, relaciones flotantes entre objetos)

Desventajas:

  • Alto riesgo de errores en la gestión de memoria
  • Difícil de mantener

Caso positivo

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:

  • Relaciones de propiedad transparentes y comprensibles
  • Sin fugas o liberaciones dobles

Desventajas:

  • No siempre es posible la composición cíclica
  • A veces es necesario mejorar los protocolos de comunicación entre objetos