POO: Composición y agregación

Publicado por Andrea Navarro el

En este artículo veremos las relaciones estructurales de agregación y composición, y cómo utilizarlos en nuestros proyectos.

La herencia no es el la única forma de relacionar clases en la programación orientada a objetos. Existen otros tipos de relaciones entre objetos que pueden crearse para representar la forma de interactuar entre objetos.

Mientras que la herencia representa una relación ES-UN (ej: un EmpleadoTemporal es un tipo de Empleado) la composición y la agregación representan relaciones del tipo TIENE-UN.

La diferencia entre agregación y composición es la fortaleza de esta relación. En la agregación la clase principal hace uso de la otra clase independiente de la primera. Es una relación USA-UN, lo que significa que el objeto que es usado existe antes de ser creada la clase que lo usa, y sigue existiendo luego que la clase que lo usa es destruida.

En la composición la relación es más fuerte, es una relación POSEE-UN. El objeto poseído es creado dentro de la clase que la posee, y es destruido junto con ella.

AgregaciónComposición
Relaciónusa unposee un
Dependenciala parte es independientela parte depende del todo
Ciclo de vidaseparadocompartido
Creaciónel objeto existe antesel objeto se crea durante
Destrucciónel objeto sobreviveel objeto de destruye

La forma de establecer esta relación entre clases es definir una de las clases como atributo de la otra. Si se quiere acceder a estos objetos se hace a través de la clase que los contiene.

Agregación

Relación uno-a-uno

En el siguiente ejemplo se muestran las clases existentes en un sistema de manejo de un conjunto de taxis.

Cada objeto Taxi existente en el sistema debe tener asociado el conductor que lo manejará. Este Conductor es a su vez un objeto.

Para definir esta relación se coloca un atributo conductor que será cargado en el __init__ del Taxi.

class Conductor:
    def __init__(self, nombre):
        self.nombre = nombre

class Taxi:
    def __init__(self, placa, conductor):
        self.placa = placa
        self.conductor = conductor

El Conductor debe ser creado primero y luego es posible crear el Taxi pasando por parámetro el conductor asociado.

conductorA = Conductor("Pedro Perez")
taxiA = Taxi("XJD48383C",conductorA)

Es posible acceder a los datos del conductor de un taxi específico a través de los atributos del taxi.

taxiA.conductor.nombre

Si el taxi creado deja de existir, el objeto Conductor asociado sigue existiendo ya que fue creado anteriormente y puede ser asignado a otro taxi.

Relación uno-a-mucho

En este ejemplo se muestran las clases de el carrito de compras de un sistema de compras online.

El carrito puede tener asociados muchos productos, por lo que el atributo no será un solo objeto Producto sino una lista que contendrá objetos Producto. Esta lista no se carga en el __init__ del Carrito, sino que se define la función agregar_producto() para rellenar esta lista.

Cada producto deberá ser creado antes de poder ser ingresado en el carrito, y seguirán existiendo aunque se destruya el carrito asociado. Esto es necesario ya que el mismo producto deberá poder ser agregado a muchos carritos.

class Producto:
    def __init__(self, nombre, precio):
        self.nombre = nombre
        self.precio = precio

class CarritoCompras:
    def __init__(self):
        self.productos = []
    
    def agregar_producto(self, producto):
       self.productos.append(producto)

Primero se crean los productos con sus atributos, luego se crea el carrito y finalmente se agregan los productos al carrito.

leche = Producto("Leche", 2000)
pan = Producto("Pan", 1500)

carrito = CarritoCompras()
carrito.agregar_producto(leche)  
carrito.agregar_producto(pan) 

Composición

Relación uno-a-uno

En este ejemplo de composición se muestran las clases correspondientes al sistema de ingreso a una cuenta bancaria con el uso de un PIN.

La cuenta bancaria es una clase que posee un atributo de número de cuenta, y un PIN asociado, este PIN pertenece a la cuenta y no existe fuera de ella. Es, a su vez, una clase que tiene un valor (el código de acceso a la cuenta) y almacena los intentos fallidos de ingreso al sistema.

A diferencia de los casos de agregación vistos anteriormente este objeto no se crea afuera de la clase y luego es pasada por parámetro, sino que es creada directamente dentro de la clase CuentaBancaria.

Dentro del __init__ de la misma puede verse como el atributo PIN es definido como self.pin = PIN(1234), llamando al __init__ de PIN y guardando el objeto creado como un atributo.

class PIN:
    def __init__(self, valor):
        self.valor = valor
        self.intentos_fallidos = 0
    
    def verificar(self, intento):
        if self.intentos_fallidos >= 3:
            return False, "Cuenta bloqueada"
        
        if intento == self.valor:
            self.intentos_fallidos = 0
            return True, "PIN correcto"
        else:
            self.intentos_fallidos += 1
            return False, f"PIN incorrecto. Intentos: {self.intentos_fallidos}/3"

class CuentaBancaria:
    def __init__(self, numero_cuenta):
        self.numero = numero_cuenta        
        self.pin = PIN(1234)
    
    def ingresar(self,  pin_intento):
        correcto, mensaje = self.pin.verificar(pin_intento)        
        if not correcto:
            return mensaje        
        return "Ingreso exitoso"

Para utilizar esta relación solamente se debe crear la CuentaBancaria.

cuenta = CuentaBancaria("001-123456")
print(cuenta.ingresar(1233))  
print(cuenta.ingresar(1234))  

Relación uno-a-muchos

También es posible tener una composición donde el objeto principal posea más de un objeto.

En este ejemplo se define una Factura que contiene diferentes detalles. Cada detalle esta representado por una clase DetalleFactura que contiene la descripción, cantidad y precio de cada item de la Factura.

Al igual que en el caso anterior, los DetalleFactura no existen fuera de la Factura que los contiene, ni deben existir luego de que esta es destruida.

Dentro del __init__ de Factura se define el atributo detalles que es la lista que contendrá los objetos FacturaDetalles asociados. En el método agregar_detalle se creará el nuevo objeto FacturaDetalle y se agregará a la lista.

class DetalleFactura:
    def __init__(self, descripcion, cantidad, precio):
        self.descripcion = descripcion
        self.cantidad = cantidad
        self.precio = precio

class Factura:
    def __init__(self, numero):
        self.numero = numero
        self.detalles = [] 
    
    def agregar_detalle(self, descripcion, cantidad, precio):
        nuevo_detalle = DetalleFactura(descripcion, cantidad, precio)
        self.detalles.append(nuevo_detalle)

Para crear una factura se creará el objeto Factura y luego se utilizará la función agregar_detalle para crear cada uno de los objeto DetalleFactura.

factura = Factura("FAC-001")
factura.agregar_detalle("Producto A", 2, 10.0)  
factura.agregar_detalle("Servicio B", 1, 25.0) 

En este artículo hemos visto la diferencia entre composición y agregación y cómo utilizarlas para definir relaciones entre objetos. También hemos visto como utilizarlas tanto en relaciones de uno-a-uno como uno-a-muchos.

Espero que les sea de utilidad!


¿Querés aprender más? 📚

👉 Visitá nuestros cursos!
💬 Y si tenés dudas, o querés dejarnos tus comentarios sumate a la Comunidad JuncoTIC en Telegram!
¡Te esperamos!

Categorías: Programación

Andrea Navarro

- Ingeniera en Informática - Docente universitaria - Investigadora