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.