Patrones de diseño comunes en Python
Introducción
Los patrones de diseño son soluciones probadas a problemas comunes que surgen durante el desarrollo de software. Representan las mejores prácticas utilizadas por desarrolladores experimentados para resolver desafíos recurrentes en la programación. En Python, estos patrones adquieren características particulares gracias a la flexibilidad y expresividad del lenguaje. Conocer estos patrones no solo mejorará la calidad de tu código, sino que también te permitirá comunicarte más eficazmente con otros desarrolladores mediante un vocabulario común de soluciones.
En este artículo, exploraremos los patrones de diseño más utilizados en Python, cómo implementarlos correctamente y cuándo es apropiado aplicar cada uno de ellos.
Patrones creacionales
Los patrones creacionales se enfocan en mecanismos de creación de objetos.
Patrón Singleton
Este patrón garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella.
class Singleton:
_instancia = None
def __new__(cls):
if cls._instancia is None:
cls._instancia = super(Singleton, cls).__new__(cls)
return cls._instancia
# Uso del patrón
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # True - son la misma instancia
En Python, también podemos implementar singletons utilizando un decorador:
def singleton(cls):
instancias = {}
def obtener_instancia(*args, **kwargs):
if cls not in instancias:
instancias[cls] = cls(*args, **kwargs)
return instancias[cls]
return obtener_instancia
@singleton
class BaseDatos:
def __init__(self):
self.conexion = "Conectado a la base de datos"
# Ambas variables apuntan a la misma instancia
db1 = BaseDatos()
db2 = BaseDatos()
Patrón Factory Method
Proporciona una interfaz para crear objetos, pero permite a las subclases decidir qué clase instanciar.
class Animal:
def hablar(self):
pass
class Perro(Animal):
def hablar(self):
return "Guau"
class Gato(Animal):
def hablar(self):
return "Miau"
class FabricaAnimales:
def crear_animal(self, tipo):
if tipo == "perro":
return Perro()
elif tipo == "gato":
return Gato()
else:
raise ValueError(f"Tipo de animal desconocido: {tipo}")
# Uso del patrón
fabrica = FabricaAnimales()
mi_mascota = fabrica.crear_animal("perro")
print(mi_mascota.hablar()) # Guau
Patrones estructurales
Los patrones estructurales se ocupan de la composición de clases y objetos.
Patrón Adapter (Adaptador)
Permite que interfaces incompatibles trabajen juntas convirtiendo la interfaz de una clase en otra que el cliente espera.
# Supongamos que tenemos una API externa con una interfaz diferente
class APISistemaPago:
def realizar_cobro(self, cantidad, cuenta):
return f"Cobrando {cantidad}€ de la cuenta {cuenta}"
# La interfaz que nuestra aplicación espera
class ProcesadorPago:
def procesar(self, importe, usuario):
pass
# Adaptador que hace compatible la API externa con nuestra interfaz
class AdaptadorPago(ProcesadorPago):
def __init__(self, api_sistema_pago):
self.api = api_sistema_pago
def procesar(self, importe, usuario):
# Convertimos los parámetros al formato esperado por la API externa
cuenta = f"cuenta-{usuario}"
return self.api.realizar_cobro(importe, cuenta)
# Uso del patrón
api_externa = APISistemaPago()
procesador = AdaptadorPago(api_externa)
resultado = procesador.procesar(100, "usuario123")
print(resultado) # Cobrando 100€ de la cuenta cuenta-usuario123
Patrón Decorator (Decorador)
Python tiene soporte nativo para decoradores, lo que facilita la implementación de este patrón. Permite añadir nuevas funcionalidades a objetos sin modificar su estructura.
def registrar(funcion):
def envolver(*args, **kwargs):
print(f"Llamando a la función: {funcion.__name__}")
resultado = funcion(*args, **kwargs)
print(f"Función {funcion.__name__} ejecutada con resultado: {resultado}")
return resultado
return envolver
@registrar
def sumar(a, b):
return a + b
# Uso del patrón
resultado = sumar(5, 3) # Imprime información antes y después de la ejecución
Patrones de comportamiento
Estos patrones se enfocan en la comunicación entre objetos.
Patrón Observer (Observador)
Define una dependencia uno a muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados.
class Sujeto:
def __init__(self):
self._observadores = []
def registrar(self, observador):
self._observadores.append(observador)
def eliminar(self, observador):
self._observadores.remove(observador)
def notificar(self, mensaje):
for observador in self._observadores:
observador.actualizar(mensaje)
class Observador:
def actualizar(self, mensaje):
pass
class ObservadorConcreto(Observador):
def __init__(self, nombre):
self.nombre = nombre
def actualizar(self, mensaje):
print(f"{self.nombre} recibió: {mensaje}")
# Uso del patrón
canal = Sujeto()
suscriptor1 = ObservadorConcreto("Ana")
suscriptor2 = ObservadorConcreto("Pedro")
canal.registrar(suscriptor1)
canal.registrar(suscriptor2)
canal.notificar("¡Nuevo vídeo disponible!")
# Ana recibió: ¡Nuevo vídeo disponible!
# Pedro recibió: ¡Nuevo vídeo disponible!
Patrón Strategy (Estrategia)
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables.
class EstrategiaOrdenacion:
def ordenar(self, datos):
pass
class OrdenacionAscendente(EstrategiaOrdenacion):
def ordenar(self, datos):
return sorted(datos)
class OrdenacionDescendente(EstrategiaOrdenacion):
def ordenar(self, datos):
return sorted(datos, reverse=True)
class Contexto:
def __init__(self, estrategia):
self.estrategia = estrategia
def establecer_estrategia(self, estrategia):
self.estrategia = estrategia
def ejecutar_ordenacion(self, datos):
return self.estrategia.ordenar(datos)
# Uso del patrón
numeros = [1, 5, 3, 9, 2]
contexto = Contexto(OrdenacionAscendente())
print(contexto.ejecutar_ordenacion(numeros)) # [1, 2, 3, 5, 9]
contexto.establecer_estrategia(OrdenacionDescendente())
print(contexto.ejecutar_ordenacion(numeros)) # [9, 5, 3, 2, 1]
Patrones específicos de Python
Python tiene características únicas que permiten implementar patrones de forma más sencilla o incluso patrones específicos.
Context Managers (Administradores de contexto)
Permiten asignar y liberar recursos cuando sea necesario:
class Archivo:
def __init__(self, nombre, modo):
self.nombre = nombre
self.modo = modo
def __enter__(self):
self.archivo = open(self.nombre, self.modo)
return self.archivo
def __exit__(self, tipo_exc, valor_exc, traza_exc):
self.archivo.close()
# Uso del patrón
with Archivo("datos.txt", "w") as f:
f.write("Texto de ejemplo")
# El archivo se cierra automáticamente al salir del bloque with
Descriptors (Descriptores)
Permiten personalizar el comportamiento de acceso a atributos:
class ValidadorEdad:
def __set_name__(self, propietario, nombre):
self.nombre_privado = f"_{nombre}"
def __get__(self, instancia, propietario):
return getattr(instancia, self.nombre_privado)
def __set__(self, instancia, valor):
if valor < 0:
raise ValueError("La edad no puede ser negativa")
setattr(instancia, self.nombre_privado, valor)
class Persona:
edad = ValidadorEdad()
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
# Uso del patrón
ana = Persona("Ana", 25)
print(ana.edad) # 25
try:
ana.edad = -5 # Lanza una excepción
except ValueError as e:
print(str(e)) # La edad no puede ser negativa
¿Cuándo usar patrones de diseño?
Los patrones de diseño son herramientas poderosas, pero es importante aplicarlos correctamente:
- No uses patrones por usarlos: Implementa un patrón solo cuando realmente resuelva un problema que tienes.
- Simplicidad primero: El código simple y directo es preferible si no necesitas la flexibilidad de un patrón.
- Considera el contexto: No todos los patrones son apropiados para todas las situaciones.
- Documenta tu implementación: Cuando uses un patrón, comenta por qué lo elegiste.
Resumen
Los patrones de diseño son soluciones reutilizables a problemas comunes en el desarrollo de software. En Python, gracias a sus características como lenguaje dinámico y de alto nivel, muchos patrones se pueden implementar de forma más sencilla y elegante. Conocer estos patrones te ayudará a escribir código más mantenible, flexible y fácil de entender, además de facilitar la comunicación con otros desarrolladores mediante un vocabulario común. A medida que avances en tu experiencia con Python, irás reconociendo situaciones en las que estos patrones pueden aplicarse para mejorar la calidad de tus programas.