Los Patrones de Estrategia y Despacho
Reemplace las cadenas if/elif en crecimiento con registros de estrategia (diccionarios de llamables) o functools.singledispatch para el despacho basado en tipos. Ambos mantienen el diseño abierto/cerrado: agregue una nueva variante sin editar el núcleo del despachador.
Receta
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
from functools import singledispatch
STRATEGIES = {
"email": lambda user: f"email:{user['email']}",
"sms": lambda user: f"sms:{user['phone']}",
}
def notify(user: dict, channel: str) -> str:
return STRATEGIES[channel](user)
@singledispatch
def serialize(obj) -> str:
raise TypeError(type(obj))
@serialize.register(int)
def _(obj: int) -> str:
return str(obj)Cuándo usar esto:
- Diccionario de estrategia cuando el comportamiento varía según una clave de cadena/enum (método de pago, formato de exportación)
- singledispatch cuando el comportamiento varía según el tipo de argumento
- Patrón de registro cuando los plugins registran manejadores al importar
- Reemplazar 5+ ramas elif que solo difieren por la función que se ejecuta
- Testeabilidad cuando cada estrategia es una función pequeña y pura
Ejemplo de Trabajo
Estrategias de notificación más singledispatch para dar forma a las respuestas de la API.
from dataclasses import dataclass
from functools import singledispatch
from typing import Callable
@dataclass(frozen=True)
class User:
name: str
email: str
phone: str
Notifier = Callable[[User, str], None]
def log_notify(user: User, message: str) -> None:
print(f"[log] {user.name}: {message}")
def email_notify(user: User, message: str) -> None:
print(f"[email] to {user.email}: {message}")
CHANNELS: dict[str, Notifier] = {
"log": log_notify,
"email": email_notify,
}
def dispatch_notify(channel: str, user: User, message: str) -> None:
try:
handler = CHANNELS[channel]
except KeyError as exc:
raise ValueError(f"canal desconocido: {channel}") from exc
handler(user, message)
@singledispatch
def to_json(obj) -> dict:
raise TypeError(f"no soportado: {type(obj)!r}")
@to_json.register(User)
def _(user: User) -> dict:
return {"name": user.name, "email": user.email}
@to_json.register(int)
def _(value: int) -> dict:
return {"type": "int", "value": value}
ada = User("Ada", "ada@example.com", "+15551234")
dispatch_notify("email", ada, "hello")
print(to_json(ada))
print(to_json(42))Lo que esto demuestra:
- La búsqueda en diccionario reemplaza a elif en cadenas de canal
- Las claves desconocidas generan
ValueErrorclaras con encadenamiento singledispatchagrega tipos sin tocar la función base- El tipado
Callabledocumenta la firma de la estrategia
Inmersión Profunda
Cómo Funciona
- Patrón de estrategia: selecciona un algoritmo en tiempo de ejecución a través de un mapa o atributo de objeto
singledispatchalmacena implementaciones registradas indexadas por tipo- La resolución MRO elige el tipo registrado más específico
- Los registros se pueden poblar en la importación del módulo o mediante decoradores de plugins
- Ambos patrones localizan el cambio: nueva variante = nueva entrada, no despachador editado
Estrategia vs singledispatch
| Mecanismo | Clave | Mejor para |
|---|---|---|
| Diccionario de llamables | Cadena/enum | Impulsado por configuración, nombres de plugins |
| singledispatch | Tipo | Serialización, formato |
| match/case | Estructura | Formas de patrón complejas (3.10+) |
Notas de Python
from enum import StrEnum
class Format(StrEnum):
JSON = "json"
CSV = "csv"
EXPORTERS: dict[Format, Callable] = {
Format.JSON: export_json,
Format.CSV: export_csv,
}Trampas
- Registros de valores predeterminados mutables - los diccionarios compartidos mutados en tiempo de ejecución filtran el estado entre pruebas. Solución: copie los registros o use mapeos inmutables (
MappingProxyType). - KeyError vs error explícito - el acceso directo al diccionario produce errores vagos. Solución: envuélvalo con mensajes
ValueErrorútiles. - singledispatch en métodos - registrar en métodos es incómodo; use funciones independientes o
singledispatchmethod. - elif todavía creciendo en otros lugares - el despacho solo ayuda en un límite; las ramas duplicadas en los llamadores significan un nivel de abstracción incorrecto. Solución: despache una vez en la fachada.
- Rama predeterminada sin probar - la base
singledispatchque siempre genera un error oculta los registros faltantes hasta el tiempo de ejecución. Solución: pruebe cada tipo registrado.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
match/case | Variantes fijas y estructurales | Plugins añadidos dinámicamente en tiempo de ejecución |
| Polimorfismo OOP | Comportamiento ligado a la jerarquía de clases | Las funciones son suficientes |
Cadena if/elif | 2-3 ramas simples | Las ramas siguen creciendo |
| Patrón Visitor | Doble despacho sobre árboles tipo AST | Despacho de enum plano |
Preguntas Frecuentes
¿Cuándo es un diccionario mejor que singledispatch?
Cuando la clave es una cadena, un valor de configuración o un nombre de plugin, no un tipo de Python. Los proveedores de pago, los formatos de exportación y los subcomandos de CLI encajan en diccionarios.
¿Puedo registrar estrategias en tiempo de ejecución?
Sí. Tanto los diccionarios como singledispatch.register admiten el registro en tiempo de ejecución para sistemas de plugins.
¿Cómo pruebo los despachadores?
Pruebe cada función de estrategia de forma aislada, luego una prueba de integración por clave/tipo a través del despachador.
¿Funciona singledispatch con ABCs?
Sí. El registro en clases base se aplica a las subclases a menos que exista un registro más específico.
¿Qué pasa con los manejadores asíncronos?
Mantenga las estrategias asíncronas como funciones asíncronas; el despachador debe awaitlas. Considere registros asíncronos separados.
¿Es match/case un reemplazo?
Para variantes estáticas y conocidas, match es legible. Para conjuntos abiertos de plugins cargados desde puntos de entrada, los registros de diccionario siguen siendo mejores.
¿Cómo documento las claves válidas?
Use tipos Literal, enums, o exponga CHANNELS.keys() en la documentación de la API y la validación.
¿Pueden las estrategias ser lambdas?
Sí, para las de una sola línea. Nombre las funciones cuando la lógica crezca: los rastreos de pila y las pruebas necesitan nombres legibles.
¿Qué pasa con las caídas por defecto?
.get(key, default_fn) para diccionarios. Para singledispatch, mantenga una implementación base sensata o un TypeError explícito.
¿Cómo se relaciona esto con el patrón OOP de Estrategia?
El diccionario de llamables es el patrón de Estrategia funcional: misma intención, menos código repetitivo de clases en Python.
Relacionado
- Patrones Factory y Builder - construcción de objetos de estrategia
- Observer y Pub/Sub - despacho de eventos
- Inyección de Dependencias en Python - inyectar estrategias
- Antipatrones Comunes - proliferación de elif
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.