Los patrones de diseño son soluciones probadas a problemas recurrentes en el diseño de software orientado a objetos. No son código que copias y pegas, sino una forma de pensar la estructura de tus clases para que el código sea más flexible, reutilizable y fácil de mantener.
Se agrupan tradicionalmente en tres categorías (catálogo GoF — Gang of Four):
- Creacionales: cómo se crean los objetos (Singleton, Factory Method, Builder...).
- Estructurales: cómo se componen clases y objetos (Decorator, Adapter, Facade...).
- De comportamiento: cómo interactúan y se reparten responsabilidades (Strategy, Observer, Command...).
Python no obliga a seguir estos patrones al pie de la letra como Java (por ejemplo, muchas veces un módulo o una función resuelven lo que en Java necesitaría una clase completa), pero conocerlos ayuda a reconocer el problema y elegir la solución más simple. Revisamos ocho de los más usados, cada uno con un ejemplo de código.
1. Singleton
Garantiza que una clase tenga una sola instancia en toda la aplicación. En Python rara vez se necesita el patrón clásico (un módulo importado ya actúa como singleton), pero cuando sí se requiere controlarlo a nivel de clase, se puede sobrescribir __new__.
import os
import threading
class ConfiguracionApp:
_instancia = None
_lock = threading.Lock()
def __new__(cls):
if cls._instancia is None:
with cls._lock:
if cls._instancia is None:
cls._instancia = super().__new__(cls)
cls._instancia.entorno = os.environ.get("APP_ENV", "produccion")
return cls._instancia
config = ConfiguracionApp()
print(config.entorno)
El lock evita que dos hilos creen la instancia al mismo tiempo. Si no trabajas con hilos, basta con el chequeo simple sin threading.Lock.
2. Factory Method
Delega la creación de objetos a una función o método especializado, en lugar de instanciar directamente la clase concreta. Útil cuando el tipo exacto de objeto depende de una condición o configuración.
from abc import ABC, abstractmethod
class Notificador(ABC):
@abstractmethod
def enviar(self, mensaje: str) -> None:
...
class NotificadorEmail(Notificador):
def enviar(self, mensaje: str) -> None:
print(f"Email enviado: {mensaje}")
class NotificadorSms(Notificador):
def enviar(self, mensaje: str) -> None:
print(f"SMS enviado: {mensaje}")
def crear_notificador(tipo: str) -> Notificador:
opciones = {"email": NotificadorEmail, "sms": NotificadorSms}
if tipo not in opciones:
raise ValueError(f"Tipo no soportado: {tipo}")
return opciones[tipo]()
El código que consume Notificador nunca necesita saber qué clase concreta se está usando, lo que facilita agregar nuevos tipos de notificación sin tocar el código existente (principio abierto/cerrado).
3. Builder
Permite construir objetos complejos paso a paso, evitando funciones o constructores con demasiados parámetros. En Python muchas veces se resuelve con argumentos por palabra clave o dataclasses, pero el patrón Builder sigue siendo útil cuando la construcción tiene pasos o validaciones encadenadas.
from dataclasses import dataclass
@dataclass(frozen=True)
class Pedido:
cliente: str
producto: str
cantidad: int
urgente: bool
class PedidoBuilder:
def __init__(self, cliente: str, producto: str):
self._cliente = cliente
self._producto = producto
self._cantidad = 1
self._urgente = False
def cantidad(self, cantidad: int) -> "PedidoBuilder":
self._cantidad = cantidad
return self
def urgente(self, urgente: bool) -> "PedidoBuilder":
self._urgente = urgente
return self
def build(self) -> Pedido:
return Pedido(self._cliente, self._producto, self._cantidad, self._urgente)
Uso:
pedido = (
PedidoBuilder("Max", "Teclado mecánico")
.cantidad(2)
.urgente(True)
.build()
)
4. Observer
Define una dependencia de uno-a-muchos entre objetos: cuando el "sujeto" cambia de estado, notifica automáticamente a todos sus "observadores". Es la base de los sistemas de eventos y de frameworks como Django (signals).
from typing import Callable
class Accion:
def __init__(self):
self._observadores: list[Callable[[float], None]] = []
self.precio = 0.0
def agregar_observador(self, observador: Callable[[float], None]) -> None:
self._observadores.append(observador)
def actualizar_precio(self, nuevo_precio: float) -> None:
self.precio = nuevo_precio
for observador in self._observadores:
observador(nuevo_precio)
def alerta_dashboard(precio: float) -> None:
print(f"Dashboard actualizado: ${precio}")
accion = Accion()
accion.agregar_observador(alerta_dashboard)
accion.actualizar_precio(1500)
Cualquier función o método que se suscriba a Accion recibe la notificación, sin que Accion necesite conocer sus detalles internos.
5. Strategy
Encapsula distintos algoritmos intercambiables detrás de una misma interfaz, permitiendo cambiar el comportamiento de un objeto en tiempo de ejecución. En Python, gracias a que las funciones son objetos de primera clase, muchas veces ni se necesita una clase: basta pasar la función directamente.
from typing import Callable
EstrategiaDescuento = Callable[[float], float]
def sin_descuento(monto: float) -> float:
return monto
def descuento_porcentual(porcentaje: float) -> EstrategiaDescuento:
def aplicar(monto: float) -> float:
return monto * (1 - porcentaje / 100)
return aplicar
class Carrito:
def __init__(self, estrategia: EstrategiaDescuento = sin_descuento):
self.estrategia = estrategia
def total(self, monto_original: float) -> float:
return self.estrategia(monto_original)
carrito = Carrito(estrategia=descuento_porcentual(10))
print(carrito.total(1000)) # 900.0
En vez de un enorme if/elif con todos los tipos de descuento, cada regla vive en su propia función, fácil de testear por separado.
6. Decorator
Agrega responsabilidades a un objeto dinámicamente, envolviéndolo en otro objeto con la misma interfaz, sin modificar su clase original. No confundir con los decoradores de función de Python (@algo), que son una herramienta del lenguaje distinta aunque resuelven una idea parecida.
from abc import ABC, abstractmethod
class Cafe(ABC):
@abstractmethod
def costo(self) -> float:
...
@abstractmethod
def descripcion(self) -> str:
...
class CafeBase(Cafe):
def costo(self) -> float:
return 1000
def descripcion(self) -> str:
return "Café"
class DecoradorCafe(Cafe):
def __init__(self, cafe: Cafe):
self._cafe = cafe
class ConLeche(DecoradorCafe):
def costo(self) -> float:
return self._cafe.costo() + 300
def descripcion(self) -> str:
return self._cafe.descripcion() + " con leche"
class ConChocolate(DecoradorCafe):
def costo(self) -> float:
return self._cafe.costo() + 500
def descripcion(self) -> str:
return self._cafe.descripcion() + " con chocolate"
pedido = ConChocolate(ConLeche(CafeBase()))
print(f"{pedido.descripcion()}: ${pedido.costo()}")
# Café con leche con chocolate: $1800
7. Adapter
Convierte la interfaz de una clase existente en otra interfaz que el código cliente espera, sin tocar la clase original. Muy común al integrar librerías de terceros o APIs legadas que no hablan "el idioma" de tu aplicación.
class LectorPago(ABC):
@abstractmethod
def procesar(self, monto: float) -> None:
...
# API externa ya existente, con una firma distinta que no podemos modificar
class PasarelaExterna:
def cobrar(self, monto_en_centavos: int) -> None:
print(f"Cobrando {monto_en_centavos} centavos vía pasarela externa")
class AdaptadorPasarelaExterna(LectorPago):
def __init__(self, pasarela: PasarelaExterna):
self._pasarela = pasarela
def procesar(self, monto: float) -> None:
centavos = round(monto * 100)
self._pasarela.cobrar(centavos)
El resto del sistema solo conoce LectorPago; si mañana cambias de pasarela de pago, basta con escribir un nuevo adaptador sin modificar el código que la usa.
8. Command
Encapsula una solicitud (una acción con sus parámetros) como un objeto, permitiendo parametrizar, encolar, deshacer o registrar operaciones sin que el emisor conozca los detalles de cómo se ejecutan.
class Luz:
def encender(self) -> None:
print("Luz encendida")
class Comando(ABC):
@abstractmethod
def ejecutar(self) -> None:
...
class EncenderLuz(Comando):
def __init__(self, luz: Luz):
self._luz = luz
def ejecutar(self) -> None:
self._luz.encender()
class ControlRemoto:
def __init__(self):
self._historial: list[Comando] = []
def ejecutar(self, comando: Comando) -> None:
comando.ejecutar()
self._historial.append(comando)
Como cada acción es un objeto (o, en Python, podría ser simplemente una función guardada en una lista), puedes encolarla, ejecutarla en otro hilo, guardarla en un historial para un "deshacer" o programarla para más tarde — algo mucho más difícil si la lógica estuviera en un solo método gigante con if/elif por cada botón.
¿Cuál patrón usar?
| Problema | Patrón |
|---|---|
| Necesito una única instancia compartida | Singleton |
| El tipo de objeto a crear varía según una condición | Factory Method |
| Un objeto tiene muchos parámetros opcionales | Builder |
| Varias partes del sistema deben reaccionar a un cambio | Observer |
| Quiero intercambiar algoritmos sin tocar el código cliente | Strategy |
| Necesito agregar funcionalidad sin modificar la clase original | Decorator |
| Debo integrar una API o librería con una interfaz distinta a la mía | Adapter |
| Quiero tratar acciones como objetos (encolar, deshacer, registrar) | Command |
Ningún patrón es obligatorio: úsalos cuando resuelvan un problema real de tu diseño, no como regla fija. En Python, muchas veces una función simple o un módulo resuelven lo mismo con menos código que en lenguajes puramente orientados a objetos — aplica el patrón cuando la complejidad del problema lo justifique.