Ir al contenido principal

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:

  1. No uses patrones por usarlos: Implementa un patrón solo cuando realmente resuelva un problema que tienes.
  2. Simplicidad primero: El código simple y directo es preferible si no necesitas la flexibilidad de un patrón.
  3. Considera el contexto: No todos los patrones son apropiados para todas las situaciones.
  4. 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.