Python

Python

Tutorial Python: Composición de clases

Aprende la composición de clases en Python para crear objetos complejos y flexibles en programación orientada a objetos. Ejemplos y ventajas clave.

Aprende Python y certifícate

Qué es la composición en la OOP

La composición es uno de los principios fundamentales de la Programación Orientada a Objetos (OOP) que permite construir objetos complejos a partir de otros más simples. En esencia, la composición establece una relación del tipo "tiene un" o "contiene a" entre clases, donde una clase contiene instancias de otras clases como atributos.

A diferencia de otros mecanismos de reutilización de código como la herencia (que establece relaciones "es un"), la composición se centra en combinar comportamientos de diferentes objetos para crear funcionalidades más complejas. Esta aproximación refleja mejor muchas relaciones del mundo real, donde los objetos están compuestos por otros objetos más pequeños.

Estructura básica de la composición

En Python, implementar la composición es bastante directo. Simplemente necesitamos:

  1. Definir las clases de los componentes
  2. Crear instancias de estas clases como atributos de otra clase
class Motor:
    def __init__(self, tipo="gasolina", potencia=100):
        self.tipo = tipo
        self.potencia = potencia
        self.encendido = False
        
    def arrancar(self):
        self.encendido = True
        return f"Motor de {self.tipo} arrancado"
    
    def apagar(self):
        self.encendido = False
        return "Motor apagado"


class Coche:
    def __init__(self, marca, modelo, tipo_motor="gasolina", potencia_motor=100):
        # Composición: un Coche "tiene un" Motor
        self.motor = Motor(tipo_motor, potencia_motor)
        self.marca = marca
        self.modelo = modelo
        
    def encender(self):
        return f"{self.marca} {self.modelo}: {self.motor.arrancar()}"
    
    def apagar(self):
        return f"{self.marca} {self.modelo}: {self.motor.apagar()}"

En este ejemplo, la clase Coche contiene un objeto Motor. El coche no hereda del motor (no "es un" motor), sino que "tiene un" motor como parte de su estructura.

Ventajas de la composición

La composición ofrece varias ventajas significativas:

  • Flexibilidad: Podemos cambiar los componentes en tiempo de ejecución.
  • Encapsulación más clara: Cada componente tiene responsabilidades bien definidas.
  • Menor acoplamiento: Las clases componentes pueden evolucionar independientemente.
  • Reutilización más granular: Podemos reutilizar componentes específicos sin arrastrar jerarquías completas.

Implementación en Python moderno

Python 3.13 ofrece características que hacen la composición aún más potente. Podemos aprovechar las anotaciones de tipo y los dataclasses para crear código más expresivo:

from dataclasses import dataclass
from typing import Optional


@dataclass
class Bateria:
    capacidad: int
    carga_actual: int = 0
    
    def cargar(self, cantidad: int) -> int:
        self.carga_actual = min(self.capacidad, self.carga_actual + cantidad)
        return self.carga_actual
    
    def usar(self, cantidad: int) -> bool:
        if self.carga_actual >= cantidad:
            self.carga_actual -= cantidad
            return True
        return False


@dataclass
class DispositivoElectronico:
    nombre: str
    consumo: int
    bateria: Optional[Bateria] = None
    
    def encender(self) -> str:
        if not self.bateria:
            return f"{self.nombre} no tiene batería instalada"
        
        if self.bateria.usar(self.consumo):
            return f"{self.nombre} encendido (batería: {self.bateria.carga_actual})"
        else:
            return f"Batería insuficiente para encender {self.nombre}"
    
    def instalar_bateria(self, bateria: Bateria) -> None:
        self.bateria = bateria

Este ejemplo muestra cómo un DispositivoElectronico puede componerse con una Bateria, que puede ser intercambiada o actualizada sin modificar la estructura de la clase principal.

Composición vs. agregación

Es importante distinguir entre dos tipos de relaciones "tiene un":

  • Composición fuerte: El componente no puede existir sin su contenedor (ciclo de vida dependiente). Por ejemplo, un motor que solo existe como parte de un coche específico.
  • Agregación: El componente puede existir independientemente del contenedor. Por ejemplo, una batería que puede usarse en diferentes dispositivos.
# Ejemplo de agregación
bateria_recargable = Bateria(capacidad=5000)

# La misma batería puede usarse en diferentes dispositivos
telefono = DispositivoElectronico("Teléfono", consumo=10)
telefono.instalar_bateria(bateria_recargable)

# Más tarde, podemos mover la batería a otro dispositivo
telefono.bateria = None
linterna = DispositivoElectronico("Linterna", consumo=5)
linterna.instalar_bateria(bateria_recargable)

Composición dinámica

Una característica poderosa de Python es la capacidad de componer objetos dinámicamente:

class SmartDevice:
    def __init__(self, nombre):
        self.nombre = nombre
        self.componentes = {}
    
    def agregar_componente(self, nombre, componente):
        self.componentes[nombre] = componente
        
    def usar_componente(self, nombre, *args, **kwargs):
        if nombre in self.componentes:
            return self.componentes[nombre](*args, **kwargs)
        return f"Componente {nombre} no encontrado"


# Funciones que pueden ser componentes
def camara(modo="foto"):
    return f"Tomando {modo}"

def wifi(ssid=None):
    return f"Conectando a {ssid}" if ssid else "Escaneando redes"

# Creación dinámica
smartphone = SmartDevice("Mi Teléfono")
smartphone.agregar_componente("camara", camara)
smartphone.agregar_componente("wifi", wifi)

# Uso de componentes
print(smartphone.usar_componente("camara", modo="video"))  # Tomando video
print(smartphone.usar_componente("wifi", ssid="Mi_Red"))   # Conectando a Mi_Red

Esta aproximación permite una flexibilidad extrema para componer objetos con diferentes capacidades en tiempo de ejecución.

Principio de diseño

La composición es fundamental en el principio de diseño "Favorece la composición sobre la herencia", que sugiere que debemos construir sistemas a partir de componentes pequeños y reutilizables en lugar de crear jerarquías de herencia complejas.

Este enfoque produce código más modular, mantenible y adaptable a cambios futuros, ya que es más fácil reemplazar o modificar componentes individuales que refactorizar jerarquías de herencia enteras.

Ejemplo básico ManyToOne

La relación ManyToOne (muchos a uno) es un patrón común de composición donde múltiples objetos de una clase pueden contener referencias al mismo objeto de otra clase. Este tipo de relación refleja escenarios del mundo real donde varios elementos comparten un componente común.

En Python, implementar una relación ManyToOne es sencillo y proporciona una forma elegante de modelar dependencias compartidas. Veamos un ejemplo práctico que ilustra este concepto.

Modelando una relación departamento-empleados

Consideremos un escenario empresarial donde varios empleados pertenecen a un mismo departamento:

class Departamento:
    def __init__(self, nombre, ubicacion):
        self.nombre = nombre
        self.ubicacion = ubicacion
        self.empleados = []  # Referencia inversa (no es parte de la relación ManyToOne)
    
    def agregar_empleado(self, empleado):
        if empleado not in self.empleados:
            self.empleados.append(empleado)
            # Aseguramos consistencia bidireccional
            if empleado.departamento != self:
                empleado.asignar_departamento(self)
    
    def listar_empleados(self):
        return [f"{e.nombre} ({e.puesto})" for e in self.empleados]
    
    def __str__(self):
        return f"Departamento de {self.nombre} ({self.ubicacion})"


class Empleado:
    def __init__(self, nombre, puesto, departamento=None):
        self.nombre = nombre
        self.puesto = puesto
        self.departamento = None
        # Si se proporciona un departamento, lo asignamos
        if departamento:
            self.asignar_departamento(departamento)
    
    def asignar_departamento(self, departamento):
        # Actualizamos la referencia ManyToOne
        self.departamento = departamento
        # Mantenemos la consistencia bidireccional
        if self not in departamento.empleados:
            departamento.agregar_empleado(self)
    
    def cambiar_departamento(self, nuevo_departamento):
        # Eliminamos la referencia del departamento actual
        if self.departamento:
            self.departamento.empleados.remove(self)
        # Asignamos al nuevo departamento
        self.asignar_departamento(nuevo_departamento)
    
    def __str__(self):
        dept_info = f" - {self.departamento}" if self.departamento else " - Sin departamento"
        return f"{self.nombre} ({self.puesto}){dept_info}"

Este ejemplo muestra cómo muchos empleados pueden pertenecer a un departamento, creando una relación ManyToOne. Veamos cómo funciona en la práctica:

# Creamos departamentos
tecnologia = Departamento("Tecnología", "Edificio A, Piso 3")
marketing = Departamento("Marketing", "Edificio B, Piso 2")

# Creamos empleados y los asignamos a departamentos
programador1 = Empleado("Ana García", "Desarrolladora Senior", tecnologia)
programador2 = Empleado("Carlos López", "Desarrollador Backend", tecnologia)
programador3 = Empleado("Elena Martín", "Desarrolladora Frontend", tecnologia)

diseñador = Empleado("Pablo Ruiz", "Diseñador UX", marketing)
analista = Empleado("Laura Sánchez", "Analista de Mercado", marketing)

# Verificamos la asignación
print(f"Empleados en {tecnologia.nombre}:")
for empleado in tecnologia.empleados:
    print(f"  - {empleado.nombre} ({empleado.puesto})")

# Transferimos un empleado a otro departamento
print("\nTransferencia de departamento:")
print(f"Antes: {programador3}")
programador3.cambiar_departamento(marketing)
print(f"Después: {programador3}")

# Verificamos los cambios en ambos departamentos
print(f"\nEmpleados en {tecnologia.nombre}: {len(tecnologia.empleados)}")
print(f"Empleados en {marketing.nombre}: {len(marketing.empleados)}")

La salida mostraría:

Empleados en Tecnología:
  - Ana García (Desarrolladora Senior)
  - Carlos López (Desarrollador Backend)
  - Elena Martín (Desarrolladora Frontend)

Transferencia de departamento:
Antes: Elena Martín (Desarrolladora Frontend) - Departamento de Tecnología (Edificio A, Piso 3)
Después: Elena Martín (Desarrolladora Frontend) - Departamento de Marketing (Edificio B, Piso 2)

Empleados en Tecnología: 2
Empleados en Marketing: 3

Características clave de la relación ManyToOne

En este ejemplo podemos observar varias características importantes:

  • Referencia compartida: Múltiples objetos Empleado referencian al mismo objeto Departamento.
  • Consistencia bidireccional: Cuando asignamos un empleado a un departamento, mantenemos la consistencia en ambas direcciones.
  • Transferencia de relaciones: Un empleado puede cambiar de un departamento a otro.
  • Encapsulamiento: Los métodos asignar_departamento y cambiar_departamento encapsulan la lógica de gestión de relaciones.

Implementación con tipos de datos modernos

Utilizando las características de Python 3.13, podemos mejorar nuestro ejemplo con anotaciones de tipo y otras mejoras:

from typing import List, Optional
from dataclasses import dataclass, field


@dataclass
class Departamento:
    nombre: str
    ubicacion: str
    empleados: List["Empleado"] = field(default_factory=list, repr=False)
    
    def agregar_empleado(self, empleado: "Empleado") -> None:
        if empleado not in self.empleados:
            self.empleados.append(empleado)
            if empleado.departamento != self:
                empleado.asignar_departamento(self)


@dataclass
class Empleado:
    nombre: str
    puesto: str
    departamento: Optional[Departamento] = None
    
    def __post_init__(self):
        # Establecemos la relación bidireccional al inicializar
        if self.departamento:
            self.departamento.agregar_empleado(self)
    
    def asignar_departamento(self, departamento: Departamento) -> None:
        self.departamento = departamento
        if self not in departamento.empleados:
            departamento.empleados.append(self)
    
    def cambiar_departamento(self, nuevo_departamento: Departamento) -> None:
        if self.departamento:
            self.departamento.empleados.remove(self)
        self.asignar_departamento(nuevo_departamento)

Aplicaciones prácticas de ManyToOne

Las relaciones ManyToOne son extremadamente comunes en aplicaciones del mundo real:

  • Sistema educativo: Muchos estudiantes pertenecen a una clase
  • Comercio electrónico: Muchos productos pertenecen a una categoría
  • Gestión de proyectos: Muchas tareas pertenecen a un proyecto
  • Redes sociales: Muchos comentarios pertenecen a una publicación

Ejemplo de sistema de gestión de proyectos

Veamos otro ejemplo práctico con un sistema de gestión de proyectos:

class Proyecto:
    def __init__(self, nombre, fecha_inicio):
        self.nombre = nombre
        self.fecha_inicio = fecha_inicio
        self.tareas = []
    
    def crear_tarea(self, descripcion, prioridad=1):
        # Método de fábrica que crea una tarea asociada a este proyecto
        tarea = Tarea(descripcion, prioridad, self)
        return tarea
    
    def tareas_pendientes(self):
        return [t for t in self.tareas if not t.completada]
    
    def __str__(self):
        return f"Proyecto: {self.nombre} (Tareas: {len(self.tareas)})"


class Tarea:
    def __init__(self, descripcion, prioridad, proyecto):
        self.descripcion = descripcion
        self.prioridad = prioridad
        self.completada = False
        # Establecemos la relación ManyToOne
        self.proyecto = proyecto
        # Mantenemos la consistencia bidireccional
        proyecto.tareas.append(self)
    
    def completar(self):
        self.completada = True
        return f"Tarea '{self.descripcion}' marcada como completada"
    
    def __str__(self):
        estado = "Completada" if self.completada else "Pendiente"
        return f"[{estado}] {self.descripcion} (Prioridad: {self.prioridad})"

Uso del sistema:

# Creamos un proyecto
app_movil = Proyecto("Aplicación Móvil", "2023-10-15")

# Creamos tareas asociadas al proyecto
tarea1 = app_movil.crear_tarea("Diseñar interfaz de usuario", 2)
tarea2 = app_movil.crear_tarea("Implementar autenticación", 3)
tarea3 = app_movil.crear_tarea("Configurar base de datos", 1)

# Completamos una tarea
print(tarea1.completar())

# Verificamos el estado del proyecto
print(f"\n{app_movil}")
print("Tareas pendientes:")
for tarea in app_movil.tareas_pendientes():
    print(f"  - {tarea}")

Consideraciones de rendimiento

Al implementar relaciones ManyToOne, debemos considerar:

  • Memoria: Si muchos objetos apuntan al mismo objeto compartido, este permanecerá en memoria mientras cualquiera de los objetos que lo referencian exista.
  • Modificaciones: Cambios en el objeto compartido afectarán a todos los objetos que lo referencian.
  • Ciclos de referencia: Las referencias bidireccionales pueden crear ciclos que el recolector de basura debe manejar.

Para relaciones con muchos objetos, podemos optimizar usando referencias débiles cuando sea apropiado:

import weakref

class DepartamentoOptimizado:
    def __init__(self, nombre):
        self.nombre = nombre
        # Usamos un diccionario de referencias débiles para evitar
        # mantener objetos en memoria innecesariamente
        self.empleados = weakref.WeakSet()

Esta implementación ManyToOne con referencias débiles es útil cuando tenemos miles de objetos y queremos evitar problemas de memoria.

Diferencia con la herencia

La composición y la herencia son dos mecanismos fundamentales en la Programación Orientada a Objetos que permiten la reutilización de código, pero lo hacen de maneras conceptualmente diferentes. Entender cuándo usar cada una es crucial para diseñar sistemas bien estructurados y mantenibles.

Relaciones conceptuales diferentes

La diferencia más fundamental entre ambos enfoques radica en el tipo de relación que establecen:

  • La herencia establece una relación "es un" (is-a): Un objeto de la clase derivada es también un objeto de la clase base.
  • La composición establece una relación "tiene un" (has-a): Un objeto contiene o está compuesto por otros objetos.

Veamos un ejemplo comparativo:

# Enfoque de herencia
class Vehiculo:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        
    def arrancar(self):
        return "Vehículo en marcha"
        
    def detener(self):
        return "Vehículo detenido"


class Coche(Vehiculo):  # Coche "es un" Vehículo
    def __init__(self, marca, modelo, num_puertas):
        super().__init__(marca, modelo)
        self.num_puertas = num_puertas
        
    def tocar_claxon(self):
        return "¡Beep, beep!"


# Enfoque de composición
class Motor:
    def __init__(self, tipo, cilindrada):
        self.tipo = tipo
        self.cilindrada = cilindrada
        self.encendido = False
        
    def encender(self):
        self.encendido = True
        return f"Motor {self.tipo} encendido"
        
    def apagar(self):
        self.encendido = False
        return f"Motor {self.tipo} apagado"


class Automovil:  # Automóvil "tiene un" Motor
    def __init__(self, marca, modelo, tipo_motor, cilindrada):
        self.marca = marca
        self.modelo = modelo
        self.motor = Motor(tipo_motor, cilindrada)  # Composición
        
    def arrancar(self):
        return f"{self.marca} {self.modelo}: {self.motor.encender()}"
        
    def detener(self):
        return f"{self.marca} {self.modelo}: {self.motor.apagar()}"

Flexibilidad y acoplamiento

Una de las diferencias clave entre ambos enfoques es el nivel de acoplamiento que generan:

  • La herencia crea un acoplamiento fuerte: La clase derivada depende completamente de la implementación de la clase base. Cualquier cambio en la clase base puede afectar a todas las clases derivadas.

  • La composición permite un acoplamiento más débil: Las clases se relacionan a través de interfaces bien definidas, lo que facilita la sustitución de componentes sin afectar al resto del sistema.

# Problema con herencia: cambios en la clase base afectan a las derivadas
class BaseDeDatos:
    def conectar(self):
        # Implementación original
        return "Conexión establecida"
    
    def ejecutar_consulta(self, consulta):
        # Si cambiamos esta implementación, afecta a todas las subclases
        return f"Ejecutando: {consulta}"


class BaseDeDatosMySQL(BaseDeDatos):
    # Hereda comportamiento que podría cambiar inesperadamente
    pass


# Solución con composición
class ConexionDB:
    def conectar(self):
        return "Conexión establecida"
    
    def ejecutar(self, consulta):
        return f"Ejecutando: {consulta}"


class GestorBaseDatos:
    def __init__(self, conexion):
        # Podemos cambiar la implementación de conexión sin afectar esta clase
        self.conexion = conexion
    
    def realizar_consulta(self, consulta):
        self.conexion.conectar()
        return self.conexion.ejecutar(consulta)

Extensibilidad y modificación en tiempo de ejecución

Otra diferencia importante es la capacidad de modificación en tiempo de ejecución:

  • Con la herencia, la relación entre clases se establece en tiempo de compilación y no puede cambiar durante la ejecución.
  • Con la composición, podemos modificar, reemplazar o intercambiar componentes durante la ejecución del programa.
# Composición permite cambios en tiempo de ejecución
class Dispositivo:
    def __init__(self, nombre):
        self.nombre = nombre
        self.bateria = None  # Inicialmente sin batería
    
    def instalar_bateria(self, bateria):
        self.bateria = bateria
        return f"Batería instalada en {self.nombre}"
    
    def usar(self):
        if not self.bateria:
            return f"{self.nombre} no tiene batería"
        return f"{self.nombre} funcionando con batería ({self.bateria.tipo})"


class Bateria:
    def __init__(self, tipo, capacidad):
        self.tipo = tipo
        self.capacidad = capacidad


# Podemos cambiar componentes en tiempo de ejecución
telefono = Dispositivo("Smartphone")
print(telefono.usar())  # Smartphone no tiene batería

# Instalamos una batería
bateria_litio = Bateria("Litio", 3000)
telefono.instalar_bateria(bateria_litio)
print(telefono.usar())  # Smartphone funcionando con batería (Litio)

# Cambiamos la batería por otra
bateria_alta_capacidad = Bateria("Polímero de litio", 5000)
telefono.instalar_bateria(bateria_alta_capacidad)
print(telefono.usar())  # Smartphone funcionando con batería (Polímero de litio)

Problema del diamante y herencia múltiple

La herencia múltiple puede llevar al famoso "problema del diamante", donde una clase hereda de dos clases que a su vez heredan de una clase común, creando ambigüedad:

class A:
    def metodo(self):
        return "Método de A"

class B(A):
    def metodo(self):
        return "Método de B"

class C(A):
    def metodo(self):
        return "Método de C"

class D(B, C):
    pass  # ¿Qué versión de metodo() se hereda? ¿B o C?

# Python usa el orden MRO (Method Resolution Order)
# pero esto puede ser confuso y propenso a errores

La composición evita este problema al no depender de jerarquías de herencia:

class ComponenteB:
    def metodo(self):
        return "Método de B"

class ComponenteC:
    def metodo(self):
        return "Método de C"

class D:
    def __init__(self):
        self.b = ComponenteB()
        self.c = ComponenteC()
    
    # Explícitamente elegimos qué comportamiento usar
    def metodo_desde_b(self):
        return self.b.metodo()
    
    def metodo_desde_c(self):
        return self.c.metodo()

Principio de sustitución de Liskov

La herencia debe respetar el Principio de Sustitución de Liskov, que establece que los objetos de una clase derivada deben poder sustituir a los objetos de la clase base sin alterar el comportamiento del programa:

# Violación del principio de Liskov
class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
    
    def establecer_ancho(self, ancho):
        self.ancho = ancho
    
    def establecer_alto(self, alto):
        self.alto = alto
    
    def area(self):
        return self.ancho * self.alto


class Cuadrado(Rectangulo):  # Un cuadrado "es un" rectángulo, ¿verdad?
    def __init__(self, lado):
        super().__init__(lado, lado)
    
    # Pero esto viola el principio de Liskov
    def establecer_ancho(self, ancho):
        self.ancho = ancho
        self.alto = ancho  # Un cuadrado debe mantener lados iguales
    
    def establecer_alto(self, alto):
        self.alto = alto
        self.ancho = alto  # Un cuadrado debe mantener lados iguales


# Código que funciona con rectángulos pero falla con cuadrados
def ampliar_rectangulo(rectangulo):
    ancho_original = rectangulo.ancho
    rectangulo.establecer_alto(rectangulo.alto * 2)
    # Esperamos que el área sea el doble
    assert rectangulo.area() == ancho_original * rectangulo.alto * 2

La composición puede evitar este tipo de problemas al modelar las relaciones de manera diferente:

class Forma:
    def area(self):
        pass

class Rectangulo(Forma):
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
    
    def area(self):
        return self.ancho * self.alto

class Cuadrado(Forma):
    def __init__(self, lado):
        self.lado = lado
    
    def area(self):
        return self.lado * self.lado

Cuándo usar cada enfoque

La elección entre herencia y composición depende del contexto y los requisitos específicos:

  • Usa herencia cuando:

  • Existe una verdadera relación "es un" entre las clases

  • La clase derivada es una especialización de la clase base

  • No necesitas cambiar la implementación en tiempo de ejecución

  • La clase base está diseñada para ser extendida (no es demasiado concreta)

  • Usa composición cuando:

  • Existe una relación "tiene un" entre las clases

  • Necesitas flexibilidad para cambiar comportamientos en tiempo de ejecución

  • Quieres evitar el acoplamiento fuerte entre clases

  • Necesitas reutilizar funcionalidades de varias fuentes diferentes

Ejemplo práctico: Sistema de notificaciones

Veamos un ejemplo práctico que ilustra cómo la composición puede ser superior a la herencia en ciertos escenarios:

# Enfoque con herencia (problemático)
class Notificador:
    def enviar(self, mensaje):
        pass  # Método base

class NotificadorEmail(Notificador):
    def enviar(self, mensaje):
        return f"Enviando email: {mensaje}"

class NotificadorSMS(Notificador):
    def enviar(self, mensaje):
        return f"Enviando SMS: {mensaje}"

# ¿Y si queremos enviar por ambos medios? Necesitaríamos herencia múltiple
# o crear una nueva clase para cada combinación


# Enfoque con composición (flexible)
class Canal:
    def enviar(self, mensaje):
        pass

class CanalEmail(Canal):
    def enviar(self, mensaje):
        return f"Enviando email: {mensaje}"

class CanalSMS(Canal):
    def enviar(self, mensaje):
        return f"Enviando SMS: {mensaje}"

class CanalPushNotification(Canal):
    def enviar(self, mensaje):
        return f"Enviando notificación push: {mensaje}"

class ServicioNotificacion:
    def __init__(self):
        self.canales = []
    
    def agregar_canal(self, canal):
        self.canales.append(canal)
    
    def notificar(self, mensaje):
        resultados = []
        for canal in self.canales:
            resultados.append(canal.enviar(mensaje))
        return resultados


# Uso flexible con composición
servicio = ServicioNotificacion()
servicio.agregar_canal(CanalEmail())
servicio.agregar_canal(CanalSMS())

# Podemos configurar diferentes combinaciones para diferentes casos
resultados = servicio.notificar("Sistema actualizado")
for r in resultados:
    print(r)

# Fácilmente extensible a nuevos canales
servicio.agregar_canal(CanalPushNotification())

Patrones de diseño y composición

Muchos patrones de diseño favorecen la composición sobre la herencia:

  • Patrón Strategy: Encapsula algoritmos en clases separadas que pueden intercambiarse.
  • Patrón Decorator: Añade funcionalidades a objetos existentes sin modificar su estructura.
  • Patrón Composite: Compone objetos en estructuras de árbol para representar jerarquías.
# Ejemplo del patrón Strategy usando composición
class EstrategiaOrdenamiento:
    def ordenar(self, datos):
        pass

class OrdenamientoBurbuja(EstrategiaOrdenamiento):
    def ordenar(self, datos):
        # Implementación del algoritmo de burbuja
        return sorted(datos)  # Simplificado para el ejemplo

class OrdenamientoRapido(EstrategiaOrdenamiento):
    def ordenar(self, datos):
        # Implementación de quicksort
        return sorted(datos)  # Simplificado para el ejemplo

class Ordenador:
    def __init__(self, estrategia=None):
        self.estrategia = estrategia
    
    def establecer_estrategia(self, estrategia):
        self.estrategia = estrategia
    
    def ordenar(self, datos):
        if not self.estrategia:
            raise ValueError("No se ha establecido una estrategia de ordenamiento")
        return self.estrategia.ordenar(datos)


# Uso del patrón
ordenador = Ordenador()
ordenador.establecer_estrategia(OrdenamientoBurbuja())
print(ordenador.ordenar([3, 1, 4, 1, 5, 9, 2]))

# Cambiamos la estrategia en tiempo de ejecución
ordenador.establecer_estrategia(OrdenamientoRapido())
print(ordenador.ordenar([3, 1, 4, 1, 5, 9, 2]))

Resumen comparativo

Aspecto Herencia Composición
Relación "Es un" "Tiene un"
Acoplamiento Fuerte Débil
Flexibilidad en tiempo de ejecución No
Reutilización de código Vertical (de la clase base) Horizontal (de múltiples componentes)
Extensibilidad Limitada por la jerarquía Alta, mediante adición de componentes
Mantenibilidad Puede ser difícil con jerarquías profundas Generalmente mejor con componentes bien definidos
Principio de diseño Especialización "Favorece la composición sobre la herencia"

La tendencia en el diseño de software moderno es preferir la composición sobre la herencia siempre que sea posible, ya que produce sistemas más flexibles, mantenibles y con menor acoplamiento. Sin embargo, la herencia sigue siendo útil cuando existe una verdadera relación "es un" y se implementa cuidadosamente.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Python online

Ejercicios de esta lección Composición de clases

Evalúa tus conocimientos de esta lección Composición de clases con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Módulo math

Python
Puzzle

Reto herencia

Python
Código

Excepciones

Python
Test

Introducción a Python

Python
Test

Reto variables

Python
Código

Funciones Python

Python
Puzzle

Reto funciones

Python
Código

Módulo datetime

Python
Test

Reto acumulación

Python
Código

Reto estructuras condicionales

Python
Código

Polimorfismo

Python
Test

Módulo os

Python
Test

Reto métodos dunder

Python
Código

Diccionarios

Python
Puzzle

Reto clases y objetos

Python
Código

Reto operadores

Python
Código

Operadores

Python
Test

Estructuras de control

Python
Puzzle

Funciones lambda

Python
Test

Reto diccionarios

Python
Código

Reto función lambda

Python
Código

Encapsulación

Python
Puzzle

Reto coleciones

Python
Proyecto

Reto funciones auxiliares

Python
Código

Crear módulos y paquetes

Python
Puzzle

Módulo datetime

Python
Puzzle

Excepciones

Python
Puzzle

Operadores

Python
Puzzle

Diccionarios

Python
Test

Reto map, filter

Python
Código

Reto tuplas

Python
Código

Proyecto gestor de tareas CRUD

Python
Proyecto

Tuplas

Python
Puzzle

Variables

Python
Puzzle

Tipos de datos

Python
Puzzle

Conjuntos

Python
Test

Reto mixins

Python
Código

Módulo csv

Python
Test

Módulo json

Python
Test

Herencia

Python
Test

Análisis de datos de ventas con Pandas

Python
Proyecto

Reto fechas y tiempo

Python
Proyecto

Reto estructuras de iteración

Python
Código

Funciones

Python
Test

Reto comprehensions

Python
Código

Variables

Python
Test

Reto serialización

Python
Proyecto

Módulo csv

Python
Puzzle

Reto polimorfismo

Python
Código

Polimorfismo

Python
Puzzle

Clases y objetos

Python
Código

Reto encapsulación

Python
Código

Estructuras de control

Python
Test

Importar módulos y paquetes

Python
Test

Módulo math

Python
Test

Funciones lambda

Python
Puzzle

Reto excepciones

Python
Código

Listas

Python
Puzzle

Reto archivos

Python
Proyecto

Encapsulación

Python
Test

Reto conjuntos

Python
Código

Clases y objetos

Python
Test

Instalación de Python y creación de proyecto

Python
Test

Reto listas

Python
Código

Tipos de datos

Python
Test

Crear módulos y paquetes

Python
Test

Tuplas

Python
Test

Herencia

Python
Puzzle

Reto acceso a sistema

Python
Proyecto

Proyecto sintaxis calculadora

Python
Proyecto

Importar módulos y paquetes

Python
Puzzle

Clases y objetos

Python
Puzzle

Módulo os

Python
Puzzle

Listas

Python
Test

Conjuntos

Python
Puzzle

Reto tipos de datos

Python
Código

Reto matemáticas

Python
Proyecto

Módulo json

Python
Puzzle

Todas las lecciones de Python

Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Python

Python

Introducción

Instalación Y Creación De Proyecto

Python

Introducción

Tema 2: Tipos De Datos, Variables Y Operadores

Python

Introducción

Instalación De Python

Python

Introducción

Tipos De Datos

Python

Sintaxis

Variables

Python

Sintaxis

Operadores

Python

Sintaxis

Estructuras De Control

Python

Sintaxis

Funciones

Python

Sintaxis

Estructuras Control Iterativo

Python

Sintaxis

Estructuras Control Condicional

Python

Sintaxis

Testing Con Pytest

Python

Sintaxis

Listas

Python

Estructuras De Datos

Tuplas

Python

Estructuras De Datos

Diccionarios

Python

Estructuras De Datos

Conjuntos

Python

Estructuras De Datos

Comprehensions

Python

Estructuras De Datos

Clases Y Objetos

Python

Programación Orientada A Objetos

Excepciones

Python

Programación Orientada A Objetos

Encapsulación

Python

Programación Orientada A Objetos

Herencia

Python

Programación Orientada A Objetos

Polimorfismo

Python

Programación Orientada A Objetos

Mixins Y Herencia Múltiple

Python

Programación Orientada A Objetos

Métodos Especiales (Dunder Methods)

Python

Programación Orientada A Objetos

Composición De Clases

Python

Programación Orientada A Objetos

Funciones Lambda

Python

Programación Funcional

Aplicación Parcial

Python

Programación Funcional

Entrada Y Salida, Manejo De Archivos

Python

Programación Funcional

Decoradores

Python

Programación Funcional

Generadores

Python

Programación Funcional

Paradigma Funcional

Python

Programación Funcional

Composición De Funciones

Python

Programación Funcional

Funciones Orden Superior Map Y Filter

Python

Programación Funcional

Funciones Auxiliares

Python

Programación Funcional

Reducción Y Acumulación

Python

Programación Funcional

Archivos Comprimidos

Python

Entrada Y Salida Io

Entrada Y Salida Avanzada

Python

Entrada Y Salida Io

Archivos Temporales

Python

Entrada Y Salida Io

Contexto With

Python

Entrada Y Salida Io

Módulo Csv

Python

Biblioteca Estándar

Módulo Json

Python

Biblioteca Estándar

Módulo Datetime

Python

Biblioteca Estándar

Módulo Math

Python

Biblioteca Estándar

Módulo Os

Python

Biblioteca Estándar

Módulo Re

Python

Biblioteca Estándar

Módulo Random

Python

Biblioteca Estándar

Módulo Time

Python

Biblioteca Estándar

Módulo Collections

Python

Biblioteca Estándar

Módulo Sys

Python

Biblioteca Estándar

Módulo Statistics

Python

Biblioteca Estándar

Módulo Pickle

Python

Biblioteca Estándar

Módulo Pathlib

Python

Biblioteca Estándar

Importar Módulos Y Paquetes

Python

Paquetes Y Módulos

Crear Módulos Y Paquetes

Python

Paquetes Y Módulos

Entornos Virtuales (Virtualenv, Venv)

Python

Entorno Y Dependencias

Gestión De Dependencias (Pip, Requirements.txt)

Python

Entorno Y Dependencias

Python-dotenv Y Variables De Entorno

Python

Entorno Y Dependencias

Acceso A Datos Con Mysql, Pymongo Y Pandas

Python

Acceso A Bases De Datos

Acceso A Mongodb Con Pymongo

Python

Acceso A Bases De Datos

Acceso A Mysql Con Mysql Connector

Python

Acceso A Bases De Datos

Novedades Python 3.13

Python

Características Modernas

Operador Walrus

Python

Características Modernas

Pattern Matching

Python

Características Modernas

Instalación Beautiful Soup

Python

Web Scraping

Sintaxis General De Beautiful Soup

Python

Web Scraping

Tipos De Selectores

Python

Web Scraping

Web Scraping De Html

Python

Web Scraping

Web Scraping Para Ciencia De Datos

Python

Web Scraping

Autenticación Y Acceso A Recursos Protegidos

Python

Web Scraping

Combinación De Selenium Con Beautiful Soup

Python

Web Scraping

Accede GRATIS a Python y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de composición en la programación orientada a objetos y su diferencia con la herencia.
  • Implementar composición en Python utilizando clases y objetos.
  • Identificar las ventajas de la composición frente a la herencia, incluyendo flexibilidad y menor acoplamiento.
  • Modelar relaciones ManyToOne mediante composición y mantener la consistencia bidireccional.
  • Aplicar principios de diseño que favorecen la composición para crear sistemas modulares y mantenibles.