Patrones de diseño en Python: GoF, Strategy, Observer y más

📅 Actualizado en marzo 2026 📊 Nivel: Avanzado ⏱️ 22 min de lectura

En 1994 cuatro ingenieros publicaron un libro que cambió cómo pensamos el software orientado a objetos. El GoF —Gamma, Helm, Johnson y Vlissides— documentó 23 patrones que aparecían una y otra vez en código bien diseñado. Treinta años después, esos patrones siguen siendo relevantes, pero Python los implementa de forma muy diferente a cómo los describieron originalmente. Algunas veces con menos código. Otras veces directamente con construcciones del propio lenguaje. Y en unos pocos casos, con patrones que en Python directamente no tienen sentido.

Esta lección no es un catálogo de los 23 patrones. Es una guía práctica de los que realmente usarás, con código Python moderno, y con honestidad sobre cuándo el lenguaje ya resuelve el problema sin necesidad de introducir una clase abstracta extra.

Medallón de Platón de Atenas, filósofo griego fundador de la Academia y autor de la Teoría de las Ideas
Τὰ εἴδη ἔστιν ἀεί, γέγονε δὲ οὐδέποτε
«Las Formas siempre son; nunca llegan a ser»
Platón de Atenas · Filósofo griego · 428 a.C. – 348 a.C.
Platón distinguió dos mundos: el de las Ideas —eternas, perfectas, inmutables— y el de las cosas concretas —temporales, imperfectas, que copian a las Ideas sin llegar a ser ellas. Un caballo real es una copia imperfecta de la Idea de Caballo; un triángulo dibujado es una sombra del Triángulo perfecto. Los patrones de diseño son las Ideas platónicas del software: existen como formas puras de solución, eternas y reutilizables, mientras que las clases concretas de tu proyecto son copias imperfectas de esas formas. Singleton, Factory, Observer no son código; son arquetipos de solución. Tu implementación concreta —en Python, con tus clases y tu dominio— es la sombra de esa Forma en la pared de la caverna. Entender el patrón es acceder a la Idea; escribir el código es proyectarla en el mundo sensible.

¿Qué son los patrones de diseño y por qué importan?

Un patrón de diseño es una solución reutilizable a un problema recurrente de diseño de software. No es un algoritmo —no te dice exactamente qué código escribir— sino una plantilla de solución que puedes adaptar a tu contexto. El patrón Observer, por ejemplo, describe cómo estructurar la relación entre un objeto que cambia de estado y los objetos que necesitan ser notificados de ese cambio. Tú decides los nombres, los tipos, la implementación concreta.

El valor de los patrones no está en el código que generan, sino en el vocabulario que crean. Cuando digo "voy a usar un Strategy aquí", cualquier desarrollador que conozca el patrón entiende inmediatamente qué está pasando: hay un algoritmo intercambiable, la lógica de selección está separada de la lógica de ejecución. Eso es comunicación de alta densidad.

# ── Por qué los patrones existen: el problema del código sin estructura ──

# Código sin patrón: lógica de creación duplicada en todas partes
def procesar_pago_tarjeta(monto, numero, cvv):
    conexion = ConexionStripe()
    resultado = conexion.cobrar(monto, numero, cvv)
    return resultado

def procesar_pago_paypal(monto, email):
    conexion = ConexionPayPal()
    resultado = conexion.ejecutar_pago(monto, email)
    return resultado

def procesar_pago_bizum(monto, telefono):
    conexion = ConexionBizum()
    resultado = conexion.transferir(monto, telefono)
    return resultado

# Problema: si añades un nuevo proveedor, tienes que recordar todos los
# sitios donde crearías un objeto de conexión. Y si la interfaz de
# ConexionStripe cambia, buscas los 47 sitios donde está referenciada.


# ── Con Factory: la creación centralizada ──
class ProcesorPago:
    """Factory que centraliza la creación de procesadores."""

    _procesadores = {}   # registro de procesadores

    @classmethod
    def registrar(cls, nombre: str, procesador_class):
        cls._procesadores[nombre] = procesador_class

    @classmethod
    def crear(cls, nombre: str) -> "PasarelaPago":
        if nombre not in cls._procesadores:
            raise ValueError(f"Procesador '{nombre}' no registrado")
        return cls._procesadores[nombre]()


# Registro de implementaciones (podría cargarse desde config)
ProcesorPago.registrar("stripe", PasarelaStripe)
ProcesorPago.registrar("paypal", PasarelaPayPal)
ProcesorPago.registrar("bizum", PasarelaBizum)

# Uso: el código de negocio no conoce las implementaciones concretas
procesador = ProcesorPago.crear("stripe")
procesador.cobrar(monto=29.99, datos=datos_pago)

# Añadir un nuevo proveedor: una línea de registro. Nada más.

Los 23 patrones del GoF se dividen en tres familias según el problema que resuelven. Los creacionales abstraen el proceso de instanciación de objetos. Los estructurales describen cómo componer clases y objetos para construir estructuras más grandes. Los de comportamiento definen cómo los objetos colaboran y se distribuyen responsabilidades.

Mapa de las tres familias de patrones de diseño GoF en Python: creacionales (Singleton, Factory, Builder), estructurales (Adapter, Decorator, Facade) y de comportamiento (Observer, Strategy, Command, Template Method)
Las tres familias GoF y los patrones más usados en Python. El nodo central conecta con las categorías; las notas laterales muestran cómo Python idiomático reduce la necesidad de algunos patrones estructurales de Java/C++. Infografía: Ciberaula.

Creacionales: Singleton, Factory y Builder

Los patrones creacionales abstraen y encapsulan el proceso de creación de objetos. En Java, donde los constructores son rígidos y la herencia es la única forma de variar comportamiento, estos patrones son imprescindibles. En Python, son igualmente útiles pero suelen implementarse con menos código.

# ═══════════════════════════════════════════════
# SINGLETON — una sola instancia por proceso
# ═══════════════════════════════════════════════

# Implementación con __new__
class Configuracion:
    _instancia = None

    def __new__(cls):
        if cls._instancia is None:
            cls._instancia = super().__new__(cls)
            cls._instancia._datos = {}
        return cls._instancia

    def set(self, clave: str, valor) -> None:
        self._datos[clave] = valor

    def get(self, clave: str, default=None):
        return self._datos.get(clave, default)


c1 = Configuracion()
c2 = Configuracion()
c1.set("debug", True)
print(c2.get("debug"))   # True — misma instancia
print(c1 is c2)          # True


# ── La forma Python: un módulo ya ES un singleton ──
# config.py
_datos: dict = {}

def set(clave: str, valor) -> None:
    _datos[clave] = valor

def get(clave: str, default=None):
    return _datos.get(clave, default)

# En cualquier módulo:
# import config
# config.set("debug", True)
# config.get("debug")   ← siempre el mismo estado


# ═══════════════════════════════════════════════
# FACTORY METHOD — crear sin clase concreta
# ═══════════════════════════════════════════════

from abc import ABC, abstractmethod

class Notificacion(ABC):
    @abstractmethod
    def enviar(self, mensaje: str) -> None: ...

class NotificacionEmail(Notificacion):
    def enviar(self, mensaje: str) -> None:
        print(f"📧 Email: {mensaje}")

class NotificacionSMS(Notificacion):
    def enviar(self, mensaje: str) -> None:
        print(f"📱 SMS: {mensaje}")

class NotificacionPush(Notificacion):
    def enviar(self, mensaje: str) -> None:
        print(f"🔔 Push: {mensaje}")


# Factory con registro de clases (más Pythónico que if/elif)
class NotificacionFactory:
    _registro: dict[str, type[Notificacion]] = {
        "email": NotificacionEmail,
        "sms":   NotificacionSMS,
        "push":  NotificacionPush,
    }

    @classmethod
    def crear(cls, canal: str) -> Notificacion:
        if canal not in cls._registro:
            raise ValueError(f"Canal desconocido: {canal!r}")
        return cls._registro[canal]()

    @classmethod
    def registrar(cls, canal: str, clase: type[Notificacion]) -> None:
        """Extensión sin modificar la Factory — principio Open/Closed."""
        cls._registro[canal] = clase


# Uso:
notif = NotificacionFactory.crear("email")
notif.enviar("Tu pedido está listo")


# ── Abstract Factory: familias de objetos relacionados ──
class TemaUI(ABC):
    @abstractmethod
    def crear_boton(self) -> "Boton": ...
    @abstractmethod
    def crear_dialogo(self) -> "Dialogo": ...

class TemaOscuro(TemaUI):
    def crear_boton(self): return BotonOscuro()
    def crear_dialogo(self): return DialogoOscuro()

class TemaClaro(TemaUI):
    def crear_boton(self): return BotonClaro()
    def crear_dialogo(self): return DialogoClaro()

# El código cliente usa la fábrica sin conocer los productos concretos:
def renderizar_ui(tema: TemaUI):
    boton = tema.crear_boton()
    dialogo = tema.crear_dialogo()
    boton.render()
    dialogo.mostrar()
# ═══════════════════════════════════════════════
# BUILDER — construcción paso a paso
# ═══════════════════════════════════════════════

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class ConsultaSQL:
    """Builder para consultas SQL — method chaining."""
    _tabla:    str        = ""
    _campos:   list[str]  = field(default_factory=list)
    _filtros:  list[str]  = field(default_factory=list)
    _orden:    str        = ""
    _limite:   Optional[int] = None

    def tabla(self, nombre: str) -> "ConsultaSQL":
        self._tabla = nombre
        return self

    def campos(self, *nombres: str) -> "ConsultaSQL":
        self._campos.extend(nombres)
        return self

    def donde(self, condicion: str) -> "ConsultaSQL":
        self._filtros.append(condicion)
        return self

    def ordenar_por(self, campo: str) -> "ConsultaSQL":
        self._orden = campo
        return self

    def limite(self, n: int) -> "ConsultaSQL":
        self._limite = n
        return self

    def build(self) -> str:
        campos = ", ".join(self._campos) if self._campos else "*"
        sql = f"SELECT {campos} FROM {self._tabla}"
        if self._filtros:
            sql += " WHERE " + " AND ".join(self._filtros)
        if self._orden:
            sql += f" ORDER BY {self._orden}"
        if self._limite:
            sql += f" LIMIT {self._limite}"
        return sql


# Uso — mucho más legible que un constructor con 8 parámetros:
consulta = (
    ConsultaSQL()
    .tabla("usuarios")
    .campos("nombre", "email", "fecha_alta")
    .donde("activo = 1")
    .donde("pais = 'ES'")
    .ordenar_por("fecha_alta DESC")
    .limite(50)
    .build()
)
print(consulta)
# SELECT nombre, email, fecha_alta FROM usuarios
# WHERE activo = 1 AND pais = 'ES'
# ORDER BY fecha_alta DESC LIMIT 50
Mano de ingeniero trazando planos técnicos de arquitectura con rotulador negro sobre papel, dibujando la estructura de un edificio
Los patrones creacionales son los planos del software: documentan la estructura de la solución antes de que exista ninguna línea de código concreta, igual que un plano arquitectónico define los muros, cargas y distribución antes de poner el primer ladrillo. Factory, Builder y Singleton son planos para construir objetos de forma controlada y predecible. Fuente: Pexels (licencia libre).

Estructurales: Adapter, Decorator y Facade

Los patrones estructurales describen cómo componer objetos para formar estructuras más grandes. Son especialmente útiles cuando integras código de terceros, cuando necesitas añadir comportamiento sin modificar código existente, o cuando quieres simplificar una API compleja.

# ═══════════════════════════════════════════════
# ADAPTER — interfaces incompatibles → compatible
# ═══════════════════════════════════════════════

# Tienes una API legacy que devuelve XML, y tu sistema espera JSON
class APILegacy:
    """API antigua — no puedes modificarla."""
    def obtener_datos(self) -> str:
        return 'Ana'


class APIModerna:
    """Interfaz que espera tu sistema."""
    def get_usuarios(self) -> list[dict]:
        raise NotImplementedError


class AdaptadorXMLaJSON(APIModerna):
    """Adapta la API legacy a la interfaz moderna."""

    def __init__(self, api_legacy: APILegacy):
        self._legacy = api_legacy

    def get_usuarios(self) -> list[dict]:
        import xml.etree.ElementTree as ET
        xml_str = self._legacy.obtener_datos()
        root = ET.fromstring(xml_str)
        return [
            {"id": int(u.get("id")), "nombre": u.find("nombre").text}
            for u in root.findall("usuario")
        ]


# Código cliente: trabaja con APIModerna sin saber que hay XML por debajo
def mostrar_usuarios(api: APIModerna) -> None:
    for user in api.get_usuarios():
        print(f"  {user['id']}: {user['nombre']}")

adaptador = AdaptadorXMLaJSON(APILegacy())
mostrar_usuarios(adaptador)   # funciona transparentemente


# ── Duck typing: cuando Python no necesita el Adapter ──
# Si el objeto ya tiene los métodos necesarios, Python lo acepta
# sin forzar herencia de una clase abstracta. El "adaptador" puede
# ser simplemente un objeto con los métodos correctos.
class ConexionPostgres:
    def execute(self, sql: str) -> list: ...

class ConexionSQLite:
    def execute(self, sql: str) -> list: ...

# Ambas son compatibles directamente — no necesitas Adapter
# porque Python usa duck typing, no tipado nominal.
# ═══════════════════════════════════════════════
# DECORATOR — añadir responsabilidades dinámicamente
# ═══════════════════════════════════════════════

import functools
import time
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


# Decoradores funcionales (la forma Python)
def cronometrar(fn: Callable[P, T]) -> Callable[P, T]:
    """Mide el tiempo de ejecución de una función."""
    @functools.wraps(fn)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        inicio = time.perf_counter()
        resultado = fn(*args, **kwargs)
        duracion = time.perf_counter() - inicio
        print(f"⏱ {fn.__name__}: {duracion:.4f}s")
        return resultado
    return wrapper


def reintentar(veces: int = 3, espera: float = 1.0):
    """Reintenta la función N veces ante excepciones."""
    def decorador(fn: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(fn)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            ultimo_error = None
            for intento in range(veces):
                try:
                    return fn(*args, **kwargs)
                except Exception as e:
                    ultimo_error = e
                    if intento < veces - 1:
                        time.sleep(espera)
            raise ultimo_error
        return wrapper
    return decorador


def cachear(fn: Callable[P, T]) -> Callable[P, T]:
    """Cache simple en memoria (memoization)."""
    cache: dict = {}
    @functools.wraps(fn)
    def wrapper(*args):
        if args not in cache:
            cache[args] = fn(*args)
        return cache[args]
    return wrapper


# Composición de decoradores — se aplican de abajo a arriba
@cronometrar
@reintentar(veces=3, espera=0.5)
@cachear
def obtener_precio(producto_id: int) -> float:
    """Consulta precio en una API externa."""
    # ... llamada a API
    return 29.99


# ── Decorator de clase (patrón GoF original) ──
from abc import ABC, abstractmethod

class ComponenteTexto(ABC):
    @abstractmethod
    def renderizar(self) -> str: ...

class TextoPlano(ComponenteTexto):
    def __init__(self, texto: str):
        self._texto = texto
    def renderizar(self) -> str:
        return self._texto

class DecoradorTexto(ComponenteTexto, ABC):
    def __init__(self, componente: ComponenteTexto):
        self._componente = componente

class Negrita(DecoradorTexto):
    def renderizar(self) -> str:
        return f"{self._componente.renderizar()}"

class Cursiva(DecoradorTexto):
    def renderizar(self) -> str:
        return f"{self._componente.renderizar()}"

# Composición dinámica de decoradores
texto = Cursiva(Negrita(TextoPlano("Hola mundo")))
print(texto.renderizar())   # Hola mundo
# ═══════════════════════════════════════════════
# FACADE — simplificar API compleja
# ═══════════════════════════════════════════════

# Sin Facade: el cliente gestiona múltiples subsistemas
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from jinja2 import Template

# Código del cliente SIN facade — verbose y acoplado
mensaje = MIMEMultipart()
mensaje["From"] = "no-reply@empresa.com"
mensaje["To"] = "cliente@ejemplo.com"
mensaje["Subject"] = "Tu pedido está listo"
template = Template("Hola {{ nombre }}, tu pedido {{ id }} está listo.")
cuerpo = template.render(nombre="Ana", id="PED-1234")
mensaje.attach(MIMEText(cuerpo, "html"))
with smtplib.SMTP("smtp.empresa.com", 587) as server:
    server.starttls()
    server.login("user", "pass")
    server.send_message(mensaje)


# Con Facade: una interfaz simple sobre el subsistema complejo
class ServicioEmail:
    """Facade sobre SMTP + Jinja2."""

    def __init__(self, host: str, puerto: int, usuario: str, password: str):
        self._host     = host
        self._puerto   = puerto
        self._usuario  = usuario
        self._password = password
        self._templates: dict[str, Template] = {}

    def registrar_template(self, nombre: str, html: str) -> None:
        self._templates[nombre] = Template(html)

    def enviar(
        self,
        destinatario: str,
        asunto: str,
        template: str,
        contexto: dict = {}
    ) -> None:
        cuerpo = self._templates[template].render(**contexto)
        msg = MIMEMultipart()
        msg["From"]    = self._usuario
        msg["To"]      = destinatario
        msg["Subject"] = asunto
        msg.attach(MIMEText(cuerpo, "html"))
        with smtplib.SMTP(self._host, self._puerto) as smtp:
            smtp.starttls()
            smtp.login(self._usuario, self._password)
            smtp.send_message(msg)


# Código cliente: limpio, desacoplado, testeable
email = ServicioEmail("smtp.empresa.com", 587, "user", "pass")
email.registrar_template(
    "pedido_listo",
    "Hola {{ nombre }}, tu pedido {{ id }} está listo."
)
email.enviar(
    destinatario="cliente@ejemplo.com",
    asunto="Tu pedido está listo",
    template="pedido_listo",
    contexto={"nombre": "Ana", "id": "PED-1234"}
)

Comportamiento: Observer, Strategy y Command

Los patrones de comportamiento son los más útiles en Python porque el lenguaje ofrece construcciones que los implementan de forma natural. Funciones como objetos de primera clase resuelve el 80% de los casos de Strategy. Callbacks y closures hacen Observer mucho más ligero. Y los callables hacen Command casi invisible.

# ═══════════════════════════════════════════════
# OBSERVER — notificar cambios a múltiples objetos
# ═══════════════════════════════════════════════

from typing import Callable, Any

class EventoBus:
    """Bus de eventos simple y genérico."""

    def __init__(self):
        self._handlers: dict[str, list[Callable]] = {}

    def suscribir(self, evento: str, handler: Callable) -> None:
        if evento not in self._handlers:
            self._handlers[evento] = []
        self._handlers[evento].append(handler)

    def desuscribir(self, evento: str, handler: Callable) -> None:
        if evento in self._handlers:
            self._handlers[evento].remove(handler)

    def emitir(self, evento: str, **datos: Any) -> None:
        for handler in self._handlers.get(evento, []):
            handler(**datos)


# Ejemplo: sistema de pedidos
bus = EventoBus()

# Observadores — funciones normales
def notificar_almacen(pedido_id: str, productos: list) -> None:
    print(f"🏭 Almacén: preparar {len(productos)} productos para {pedido_id}")

def notificar_cliente(pedido_id: str, **_) -> None:
    print(f"📧 Email confirmación enviado para pedido {pedido_id}")

def actualizar_stock(productos: list, **_) -> None:
    print(f"📊 Stock actualizado: {len(productos)} ítems reservados")

# Registro de observadores
bus.suscribir("pedido_creado", notificar_almacen)
bus.suscribir("pedido_creado", notificar_cliente)
bus.suscribir("pedido_creado", actualizar_stock)

# Publicación — un evento, múltiples reacciones
bus.emitir("pedido_creado", pedido_id="PED-001", productos=["SKU-1", "SKU-2"])
# 🏭 Almacén: preparar 2 productos para PED-001
# 📧 Email confirmación enviado para pedido PED-001
# 📊 Stock actualizado: 2 ítems reservados


# ── Observer con __setattr__: observar cambios de atributos ──
class ModeloObservable:
    """Notifica automáticamente cuando cambia cualquier atributo."""

    def __init__(self):
        self.__dict__["_observadores"] = []

    def observar(self, fn: Callable) -> None:
        self._observadores.append(fn)

    def __setattr__(self, nombre: str, valor: Any) -> None:
        antiguo = getattr(self, nombre, None)
        super().__setattr__(nombre, valor)
        for obs in self._observadores:
            obs(nombre, antiguo, valor)


usuario = ModeloObservable()
usuario.observar(lambda campo, ant, nuevo: print(f"  {campo}: {ant} → {nuevo}"))
usuario.nombre = "Ana"       # nombre: None → Ana
usuario.email = "a@b.com"   # email: None → a@b.com
# ═══════════════════════════════════════════════
# STRATEGY — algoritmo intercambiable
# ═══════════════════════════════════════════════

# En Java: interfaz Estrategia + N clases que la implementan
# En Python: funciones como objetos de primera clase

from typing import Callable

# Estrategias como funciones puras
def ordenar_por_nombre(items: list[dict]) -> list[dict]:
    return sorted(items, key=lambda x: x["nombre"])

def ordenar_por_precio(items: list[dict]) -> list[dict]:
    return sorted(items, key=lambda x: x["precio"])

def ordenar_por_relevancia(items: list[dict]) -> list[dict]:
    return sorted(items, key=lambda x: -x.get("puntuacion", 0))


class CatalogoCursos:
    def __init__(self, cursos: list[dict]):
        self._cursos = cursos
        # Estrategia por defecto
        self._estrategia: Callable = ordenar_por_nombre

    def establecer_orden(self, estrategia: Callable) -> None:
        self._estrategia = estrategia

    def listar(self) -> list[dict]:
        return self._estrategia(self._cursos)


catalogo = CatalogoCursos([
    {"nombre": "Python", "precio": 99.0, "puntuacion": 4.8},
    {"nombre": "Django", "precio": 149.0, "puntuacion": 4.9},
    {"nombre": "FastAPI", "precio": 79.0, "puntuacion": 4.7},
])

catalogo.establecer_orden(ordenar_por_precio)
print(catalogo.listar())   # FastAPI, Python, Django (ascendente)

catalogo.establecer_orden(ordenar_por_relevancia)
print(catalogo.listar())   # Django, Python, FastAPI

# Con lambdas — Strategy en una línea:
catalogo.establecer_orden(lambda items: sorted(items, key=lambda x: x["nombre"][-1]))


# ── Strategy con Protocol (verificación estática) ──
from typing import Protocol

class EstrategiaOrden(Protocol):
    def __call__(self, items: list[dict]) -> list[dict]: ...

def aplicar_estrategia(
    cursos: list[dict],
    estrategia: EstrategiaOrden
) -> list[dict]:
    return estrategia(cursos)
# ═══════════════════════════════════════════════
# COMMAND — encapsular operación como objeto
# ═══════════════════════════════════════════════

from dataclasses import dataclass
from typing import Protocol
import shutil
import os

class Comando(Protocol):
    def ejecutar(self) -> None: ...
    def deshacer(self) -> None: ...


@dataclass
class MoverArchivo:
    origen: str
    destino: str

    def ejecutar(self) -> None:
        shutil.move(self.origen, self.destino)
        print(f"  Movido: {self.origen} → {self.destino}")

    def deshacer(self) -> None:
        shutil.move(self.destino, self.origen)
        print(f"  Deshecho: {self.destino} → {self.origen}")


@dataclass
class CrearDirectorio:
    ruta: str

    def ejecutar(self) -> None:
        os.makedirs(self.ruta, exist_ok=True)
        print(f"  Creado: {self.ruta}")

    def deshacer(self) -> None:
        os.rmdir(self.ruta)
        print(f"  Eliminado: {self.ruta}")


class HistorialComandos:
    """Ejecutor con soporte de deshacer (Undo)."""

    def __init__(self):
        self._historial: list[Comando] = []

    def ejecutar(self, comando: Comando) -> None:
        comando.ejecutar()
        self._historial.append(comando)

    def deshacer_ultimo(self) -> None:
        if self._historial:
            self._historial.pop().deshacer()

    def deshacer_todo(self) -> None:
        while self._historial:
            self._historial.pop().deshacer()


# Macro: secuencia de comandos como un solo comando
historial = HistorialComandos()
historial.ejecutar(CrearDirectorio("/tmp/proyecto"))
historial.ejecutar(MoverArchivo("/tmp/datos.csv", "/tmp/proyecto/datos.csv"))
historial.deshacer_ultimo()   # deshace el MoverArchivo
historial.deshacer_todo()     # deshace el CrearDirectorio


# ── Con functools.partial: Command ultraligero ──
import functools

def mover_archivo(origen: str, destino: str) -> None:
    shutil.move(origen, destino)

# Command = función parcialmente aplicada
comando = functools.partial(mover_archivo, "/tmp/datos.csv", "/tmp/backup/datos.csv")
comando()   # ejecutar más tarde
Colección de engranajes metálicos de distintos tamaños dispuestos en fila sobre fondo negro, mostrando diferentes diámetros y dentados
Los patrones de comportamiento coordinan piezas que no se conocen directamente entre sí. El Observer desacopla el emisor de los receptores; el Strategy permite intercambiar piezas de lógica sin romper el resto; el Command empaqueta una operación en un objeto portable, aplazable y deshacible. Los engranajes de distintos tamaños engranan con precisión sin modificarse mutuamente — igual que los objetos que colaboran a través de un patrón. Fuente: Pexels (licencia libre).

Patrones idiomáticos de Python

Python tiene tres patrones que son específicos del lenguaje (o están mucho más integrados que en otros): Context Manager, Descriptor y Metaclass. No están en el catálogo GoF porque emergieron del propio diseño del lenguaje, pero son patrones de pleno derecho.

# ═══════════════════════════════════════════════
# CONTEXT MANAGER — gestión garantizada de recursos
# ═══════════════════════════════════════════════

# El problema: garantizar liberación de recursos incluso con excepciones
# Sin context manager:
conexion = ConexionBD()
try:
    datos = conexion.consultar("SELECT * FROM usuarios")
    procesar(datos)
finally:
    conexion.cerrar()   # olvidar esto = fuga de conexiones


# Con context manager (__enter__ / __exit__)
class ConexionBD:
    def __init__(self, url: str):
        self._url = url
        self._conn = None

    def __enter__(self) -> "ConexionBD":
        self._conn = conectar(self._url)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        if self._conn:
            if exc_type is None:
                self._conn.commit()
            else:
                self._conn.rollback()
            self._conn.close()
        return False   # False = propagar la excepción si hubo una

    def consultar(self, sql: str) -> list:
        return self._conn.execute(sql).fetchall()


with ConexionBD("postgresql://...") as db:
    datos = db.consultar("SELECT * FROM usuarios")
# La conexión se cierra garantizadamente, haya o no excepción


# ── Context manager con contextlib (sin clase) ──
from contextlib import contextmanager

@contextmanager
def tempdir():
    """Directorio temporal que se borra al salir."""
    import tempfile, shutil
    ruta = tempfile.mkdtemp()
    try:
        yield ruta
    finally:
        shutil.rmtree(ruta)

with tempdir() as dir_tmp:
    # Usar dir_tmp
    pass
# El directorio ya no existe
# ═══════════════════════════════════════════════
# DESCRIPTOR — atributos con comportamiento propio
# ═══════════════════════════════════════════════

# El problema: validar atributos en múltiples clases sin repetir código
# Sin descriptor — validación duplicada en cada clase:
class Producto:
    def __init__(self, nombre: str, precio: float):
        if not nombre:
            raise ValueError("nombre no puede estar vacío")
        if precio < 0:
            raise ValueError("precio no puede ser negativo")
        self._nombre = nombre
        self._precio = precio


# Con descriptor — la validación vive en UN solo lugar
class CampoTextoNoVacio:
    """Descriptor: string no vacío."""

    def __set_name__(self, owner, nombre: str) -> None:
        self._nombre = f"_{nombre}"

    def __get__(self, obj, tipo=None):
        if obj is None:
            return self
        return getattr(obj, self._nombre, None)

    def __set__(self, obj, valor: str) -> None:
        if not isinstance(valor, str) or not valor.strip():
            raise ValueError(f"{self._nombre[1:]} no puede estar vacío")
        setattr(obj, self._nombre, valor.strip())


class CampoPositivo:
    """Descriptor: número positivo."""

    def __set_name__(self, owner, nombre: str) -> None:
        self._nombre = f"_{nombre}"

    def __get__(self, obj, tipo=None):
        if obj is None:
            return self
        return getattr(obj, self._nombre, 0.0)

    def __set__(self, obj, valor: float) -> None:
        if not isinstance(valor, (int, float)) or valor < 0:
            raise ValueError(f"{self._nombre[1:]} debe ser positivo")
        setattr(obj, self._nombre, float(valor))


# Reutilizar en cualquier clase — declarativo y sin repetición
class Producto:
    nombre = CampoTextoNoVacio()
    precio = CampoPositivo()

    def __init__(self, nombre: str, precio: float):
        self.nombre = nombre   # llama a CampoTextoNoVacio.__set__
        self.precio = precio   # llama a CampoPositivo.__set__


class Curso:
    titulo    = CampoTextoNoVacio()
    duracion  = CampoPositivo()

    def __init__(self, titulo: str, duracion: float):
        self.titulo   = titulo
        self.duracion = duracion


# Esto es exactamente lo que hacen @property, @staticmethod, @classmethod:
# son descriptores integrados en el lenguaje.

Cuándo NO usar patrones de diseño

El mayor peligro de aprender los patrones GoF es aplicarlos donde no hacen falta. Hay un antipatrón conocido como patternitis: el exceso de patrones que convierte código simple en una maraña de abstracciones sin beneficio real.

# ── Sobreingeniería: cuando el patrón complica sin aportar ──

# MALO: Factory para crear un solo tipo de objeto que nunca va a variar
class CreadorSaludo:
    def crear(self, nombre: str) -> "Saludo":
        return Saludo(nombre)

class Saludo:
    def __init__(self, nombre: str):
        self.nombre = nombre
    def mostrar(self) -> str:
        return f"Hola, {nombre}!"

creador = CreadorSaludo()
saludo = creador.crear("Ana")
saludo.mostrar()

# MEJOR: simplemente...
def saludar(nombre: str) -> str:
    return f"Hola, {nombre}!"


# MALO: Observer para un objeto que solo notifica a un receptor
class Modelo:
    def __init__(self):
        self._observadores = []
    def agregar_observador(self, obs):
        self._observadores.append(obs)
    def cambiar_datos(self, datos):
        self._datos = datos
        for obs in self._observadores:
            obs.actualizar(self)

# Si siempre hay exactamente un receptor y no vas a cambiar eso:
class Modelo:
    def __init__(self, callback):
        self._callback = callback
    def cambiar_datos(self, datos):
        self._datos = datos
        self._callback(datos)


# ── Señales de que necesitas un patrón ──
# ✓ if/elif anidados para decidir qué objeto crear  → Factory
# ✓ múltiples clases notificando a múltiples receptores → Observer
# ✓ copiar y pegar el mismo algoritmo con una variación → Strategy
# ✓ código que usa recursos y puede lanzar excepciones → Context Manager
# ✓ "necesito añadir logging/caché/reintentos a esta función" → Decorator

# ── Señales de que NO necesitas un patrón ──
# ✗ tienes un solo tipo concreto que nunca cambiará → no Factory
# ✗ hay un solo receptor de eventos → no Observer, usa callback
# ✗ el algoritmo no va a cambiar nunca → no Strategy, escríbelo directo
# ✗ el recurso siempre se libera implícitamente → no Context Manager

# La regla de las tres instancias:
# la primera vez que escribes algo, escríbelo directo.
# la segunda vez que lo repites, duplícalo con una nota.
# la tercera vez, refactoriza con el patrón adecuado.

Principios SOLID y su relación con los patrones

Los patrones GoF implementan los principios SOLID de forma concreta. Entender la relación entre ambos ayuda a usar los patrones con criterio en lugar de mecánicamente.

# ── SOLID en Python: los 5 principios con ejemplos ──

# S — Single Responsibility: una clase, una razón para cambiar
# MAL: una clase que hace todo
class ProcesadorPedido:
    def calcular_total(self, items): ...
    def guardar_en_bd(self, pedido): ...
    def enviar_email_confirmacion(self, pedido): ...
    def generar_pdf_factura(self, pedido): ...

# BIEN: cada clase tiene una responsabilidad
class CalculadorPrecio:
    def calcular(self, items: list) -> float: ...

class RepositorioPedidos:
    def guardar(self, pedido) -> None: ...

class ServicioNotificacion:
    def enviar_confirmacion(self, pedido) -> None: ...


# O — Open/Closed: abierto a extensión, cerrado a modificación
# En lugar de modificar la Factory, la extiendes registrando nuevas clases
# (ejemplo ya visto en la sección de Factory)


# L — Liskov Substitution: los subtipos deben ser sustituibles
class Ave(ABC):
    @abstractmethod
    def moverse(self) -> None: ...

class Paloma(Ave):
    def moverse(self) -> None:
        print("Volando")

class Pinguino(Ave):
    def moverse(self) -> None:
        print("Nadando")   # NO lanza excepciones — sustituye correctamente

# MAL — viola Liskov:
class AveQueNoVuela(Ave):
    def moverse(self) -> None:
        raise NotImplementedError("No puedo volar")   # rompe el contrato


# I — Interface Segregation: interfaces pequeñas y específicas
# Con Protocol en Python — clientes solo dependen de lo que usan
class Legible(Protocol):
    def leer(self) -> str: ...

class Escribible(Protocol):
    def escribir(self, datos: str) -> None: ...

class Cerrable(Protocol):
    def cerrar(self) -> None: ...

# Una función que solo necesita leer no depende de cerrar/escribir
def procesar_datos(fuente: Legible) -> str:
    return fuente.leer().strip()


# D — Dependency Inversion: depender de abstracciones, no implementaciones
# MAL: la lógica de negocio depende de MySQL directamente
class ServicioUsuarios:
    def __init__(self):
        self._bd = ConexionMySQL(host="...", user="...")   # acoplamiento fuerte

# BIEN: inyectar la dependencia
class RepositorioUsuarios(Protocol):
    def buscar(self, id: int) -> dict | None: ...
    def guardar(self, usuario: dict) -> None: ...

class ServicioUsuarios:
    def __init__(self, repo: RepositorioUsuarios):
        self._repo = repo   # acepta cualquier implementación

# Testing: fácil de mockear
class RepoEnMemoria:
    def __init__(self): self._datos: dict = {}
    def buscar(self, id: int): return self._datos.get(id)
    def guardar(self, u: dict): self._datos[u["id"]] = u

servicio = ServicioUsuarios(repo=RepoEnMemoria())   # test sin BD real
Fi
         alt=
Referencia rápida: los nueve patrones GoF más frecuentes en Python, con código real y la nota Python idiomática cuando el lenguaje ofrece una alternativa más concisa. Ficha de referencia: Ciberaula.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre Patrones de diseño en Python: GoF, Strategy, Observer y más

Las dudas más comunes respondidas de forma clara y directa.

No. Los patrones de diseño son herramientas, no dogmas. El libro del GoF fue escrito pensando en C++ y Java, lenguajes con sistemas de tipos rígidos, sin funciones como objetos de primera clase, y con mucho más boilerplate. Python resuelve muchos de esos problemas con mecanismos del propio lenguaje: el patrón Strategy no necesita una clase abstracta si usas funciones; el patrón Iterator ya es parte del protocolo de Python con __iter__ y __next__; el patrón Decorator tiene soporte sintáctico nativo con @. La clave no es conocer los 23 patrones del GoF de memoria, sino entender los problemas que resuelven y saber cuándo el propio Python ya ofrece una solución más idiomática. Un programador Python maduro usa los patrones selectivamente, reconoce cuándo el lenguaje ya los implementa, y no añade complejidad por el placer de añadirla.
Un patrón de diseño opera a nivel de clases y objetos — resuelve un problema de diseño concreto y repetitivo dentro de una parte del código (cómo crear objetos, cómo componer componentes, cómo coordinar comportamientos). La arquitectura de software opera a nivel de sistema — define la estructura global de la aplicación, cómo se organizan los módulos grandes, cómo fluyen los datos, dónde está el estado. La analogía de la construcción: los patrones de diseño son técnicas de carpintería (cómo unir dos vigas, cómo hacer una puerta que encaje bien); la arquitectura es el plano del edificio (cuántas plantas tiene, dónde van los muros de carga, cómo se evacúa en caso de emergencia). MVC, hexagonal, event-driven, microservicios son arquitecturas. Observer, Factory, Decorator son patrones. Pueden combinarse: una arquitectura hexagonal puede usar internamente patrones Factory para la creación de puertos y adaptadores.
Depende de cómo se implemente, pero en muchos casos sí genera más problemas de los que resuelve. El principal problema del Singleton clásico es que introduce estado global implícito, lo que dificulta el testing (no puedes instanciar el objeto limpio entre tests), viola el principio de Inversión de Dependencias (el código que usa el Singleton depende de una implementación concreta), y puede causar problemas en entornos multihilo. La alternativa Python: un módulo importado ya es un singleton natural, porque Python cachea los módulos y solo los ejecuta una vez. Para configuración global, un módulo con variables funciona perfectamente. Para testing, es mucho más fácil parchear un módulo con unittest.mock que resetear un Singleton. Si necesitas un objeto único con estado, considera inyección de dependencias: pasar el objeto a las funciones que lo necesitan en lugar de que lo accedan globalmente.
Sí. Python tiene varios patrones idiomáticos que son exclusivos o mucho más naturales en el lenguaje. El Context Manager (with statement) es un patrón de gestión de recursos que no existe como tal en Java o C++ clásico — implementa adquisición y liberación garantizada de recursos con __enter__ y __exit__. El Descriptor Protocol (__get__, __set__, __delete__) permite crear atributos con comportamiento personalizado de forma transparente — es la base de property, staticmethod y classmethod. Las Metaclasses permiten modificar la creación de clases en tiempo de definición — el ORM de Django usa metaclasses para generar la API de modelos a partir de la definición de campos. La @property convierte métodos en atributos calculados sin cambiar la interfaz pública de la clase. Y las list/dict/set comprehensions junto con los generadores son formas idiomáticas de implementar transformaciones que en Java o C++ requerirían patrones más verbosos.
El truco no es memorizar los patrones sino reconocer los síntomas del problema que resuelven. Si tienes código que crea objetos con lógica compleja que no quieres duplicar → Factory. Si tienes una clase que hace demasiadas cosas → separa responsabilidades con Facade o descompón en objetos colaboradores. Si tienes código que necesita notificar a múltiples partes cuando algo cambia → Observer. Si tienes un algoritmo que cambia según el contexto → Strategy. Si tienes una interfaz que no encaja con la que necesitas → Adapter. Si necesitas añadir comportamiento a objetos sin modificar su clase → Decorator. La señal de alarma es cuando el código empieza a tener if/elif anidados para decidir qué tipo de objeto crear → Factory. Cuando múltiples clases comparten la misma lógica de notificación → Observer. Cuando una función recibe un parámetro para decidir qué algoritmo usar → Strategy. Los patrones emergen del código que ya funciona pero es difícil de mantener, no se imponen desde el principio.

🎯 ¿Quieres certificarte en Python?

Ciberaula ofrece cursos bonificados de Python con tutor personal, desde nivel básico hasta arquitectura de software y patrones avanzados. Formación subvencionada por FUNDAE.

Ver cursos de Python bonificados →
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Patrones de diseño en Python: GoF, Strategy, Observer y más? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!