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ícateQué 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:
- Definir las clases de los componentes
- 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 objetoDepartamento
. - 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
ycambiar_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 | Sí |
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.
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
Reto herencia
Excepciones
Introducción a Python
Reto variables
Funciones Python
Reto funciones
Módulo datetime
Reto acumulación
Reto estructuras condicionales
Polimorfismo
Módulo os
Reto métodos dunder
Diccionarios
Reto clases y objetos
Reto operadores
Operadores
Estructuras de control
Funciones lambda
Reto diccionarios
Reto función lambda
Encapsulación
Reto coleciones
Reto funciones auxiliares
Crear módulos y paquetes
Módulo datetime
Excepciones
Operadores
Diccionarios
Reto map, filter
Reto tuplas
Proyecto gestor de tareas CRUD
Tuplas
Variables
Tipos de datos
Conjuntos
Reto mixins
Módulo csv
Módulo json
Herencia
Análisis de datos de ventas con Pandas
Reto fechas y tiempo
Reto estructuras de iteración
Funciones
Reto comprehensions
Variables
Reto serialización
Módulo csv
Reto polimorfismo
Polimorfismo
Clases y objetos
Reto encapsulación
Estructuras de control
Importar módulos y paquetes
Módulo math
Funciones lambda
Reto excepciones
Listas
Reto archivos
Encapsulación
Reto conjuntos
Clases y objetos
Instalación de Python y creación de proyecto
Reto listas
Tipos de datos
Crear módulos y paquetes
Tuplas
Herencia
Reto acceso a sistema
Proyecto sintaxis calculadora
Importar módulos y paquetes
Clases y objetos
Módulo os
Listas
Conjuntos
Reto tipos de datos
Reto matemáticas
Módulo json
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
Introducción
Instalación Y Creación De Proyecto
Introducción
Tema 2: Tipos De Datos, Variables Y Operadores
Introducción
Instalación De Python
Introducción
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Estructuras Control Iterativo
Sintaxis
Estructuras Control Condicional
Sintaxis
Testing Con Pytest
Sintaxis
Listas
Estructuras De Datos
Tuplas
Estructuras De Datos
Diccionarios
Estructuras De Datos
Conjuntos
Estructuras De Datos
Comprehensions
Estructuras De Datos
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Mixins Y Herencia Múltiple
Programación Orientada A Objetos
Métodos Especiales (Dunder Methods)
Programación Orientada A Objetos
Composición De Clases
Programación Orientada A Objetos
Funciones Lambda
Programación Funcional
Aplicación Parcial
Programación Funcional
Entrada Y Salida, Manejo De Archivos
Programación Funcional
Decoradores
Programación Funcional
Generadores
Programación Funcional
Paradigma Funcional
Programación Funcional
Composición De Funciones
Programación Funcional
Funciones Orden Superior Map Y Filter
Programación Funcional
Funciones Auxiliares
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Archivos Comprimidos
Entrada Y Salida Io
Entrada Y Salida Avanzada
Entrada Y Salida Io
Archivos Temporales
Entrada Y Salida Io
Contexto With
Entrada Y Salida Io
Módulo Csv
Biblioteca Estándar
Módulo Json
Biblioteca Estándar
Módulo Datetime
Biblioteca Estándar
Módulo Math
Biblioteca Estándar
Módulo Os
Biblioteca Estándar
Módulo Re
Biblioteca Estándar
Módulo Random
Biblioteca Estándar
Módulo Time
Biblioteca Estándar
Módulo Collections
Biblioteca Estándar
Módulo Sys
Biblioteca Estándar
Módulo Statistics
Biblioteca Estándar
Módulo Pickle
Biblioteca Estándar
Módulo Pathlib
Biblioteca Estándar
Importar Módulos Y Paquetes
Paquetes Y Módulos
Crear Módulos Y Paquetes
Paquetes Y Módulos
Entornos Virtuales (Virtualenv, Venv)
Entorno Y Dependencias
Gestión De Dependencias (Pip, Requirements.txt)
Entorno Y Dependencias
Python-dotenv Y Variables De Entorno
Entorno Y Dependencias
Acceso A Datos Con Mysql, Pymongo Y Pandas
Acceso A Bases De Datos
Acceso A Mongodb Con Pymongo
Acceso A Bases De Datos
Acceso A Mysql Con Mysql Connector
Acceso A Bases De Datos
Novedades Python 3.13
Características Modernas
Operador Walrus
Características Modernas
Pattern Matching
Características Modernas
Instalación Beautiful Soup
Web Scraping
Sintaxis General De Beautiful Soup
Web Scraping
Tipos De Selectores
Web Scraping
Web Scraping De Html
Web Scraping
Web Scraping Para Ciencia De Datos
Web Scraping
Autenticación Y Acceso A Recursos Protegidos
Web Scraping
Combinación De Selenium Con Beautiful Soup
Web Scraping
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.