Os Padrões Strategy & Dispatch
Substitua cadeias crescentes de if/elif por registradores de estratégia (dicionários de chamáveis) ou functools.singledispatch para dispatch baseado em tipo. Ambos mantêm o design aberto/fechado: adicione uma nova variante sem editar o núcleo do dispatcher.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
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)Quando usar isso:
- Dicionário de estratégia quando o comportamento varia por chave de string/enum (método de pagamento, formato de exportação)
singledispatchquando o comportamento varia por tipo de argumento- Padrão de registrador quando plugins registram manipuladores na importação
- Substituindo 5+ ramificações elif que diferem apenas pela função que é executada
- Testabilidade quando cada estratégia é uma função pequena e pura
Exemplo de Trabalho
Estratégias de notificação mais singledispatch para modelagem de resposta de 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] para {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 desconhecido: {channel}") from exc
handler(user, message)
@singledispatch
def to_json(obj) -> dict:
raise TypeError(f"não suportado: {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, "olá")
print(to_json(ada))
print(to_json(42))O que isso demonstra:
- A pesquisa em dicionário substitui
elifpor strings de canal - Chaves desconhecidas geram
ValueErrorclaro com encadeamento singledispatchadiciona tipos sem tocar na função base- A tipagem
Callabledocumenta a assinatura da estratégia
Mergulho Profundo
Como Funciona
- Padrão Strategy: selecione um algoritmo em tempo de execução via um mapa ou atributo de objeto
singledispatcharmazena implementações registradas com chave por tipo- A resolução MRO escolhe o tipo registrado mais específico
- Registradores podem ser preenchidos na importação do módulo ou via decoradores de plugin
- Ambos os padrões localizam a mudança: nova variante = nova entrada, dispatcher não editado
Strategy vs singledispatch
| Mecanismo | Chave | Melhor para |
|---|---|---|
| Dicionário de chamáveis | String/enum | Orientado por configuração, nomes de plugin |
singledispatch | Tipo | Serialização, formatação |
match/case | Estrutura | Formas de padrão complexas (3.10+) |
Notas Python
from enum import StrEnum
class Format(StrEnum):
JSON = "json"
CSV = "csv"
EXPORTERS: dict[Format, Callable] = {
Format.JSON: export_json,
Format.CSV: export_csv,
}Armadilhas
- Registradores com valor padrão mutável - dicionários compartilhados mutados em tempo de execução vazam estado entre testes. Correção: copie registradores ou use mapeamentos imutáveis (
MappingProxyType). KeyErrorvs erro explícito - acesso direto a dicionário gera erros vagos. Correção: envolva com mensagensValueErrorúteis.singledispatchem métodos - registrar em métodos é complicado; use funções autônomas ousingledispatchmethod.elifainda crescendo em outros lugares - dispatch ajuda apenas em uma fronteira; ramificações duplicadas nos chamadores significam nível de abstração errado. Correção: dispatch uma vez na fachada.- Ramificação padrão não testada - a base
singledispatchque sempre levanta um erro oculta registros ausentes até o tempo de execução. Correção: teste cada tipo registrado.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
match/case | Variantes fixas e estruturais | Plugins adicionados dinamicamente em tempo de execução |
| Polimorfismo OOP | Comportamento vinculado à hierarquia de classes | Funções são suficientes |
Cadeia if/elif | 2-3 ramificações simples | Ramificações continuam crescendo |
| Padrão Visitor | Duplo dispatch sobre árvores tipo AST | Dispatch de enumeração plana |
FAQs
Quando um dicionário é melhor que `singledispatch`?
Quando a chave é uma string, valor de configuração ou nome de plugin - não um tipo Python. Provedores de pagamento, formatos de exportação e subcomandos de CLI se encaixam em dicionários.
Posso registrar estratégias em tempo de execução?
Sim. Dicionários e singledispatch.register suportam registro em tempo de execução para sistemas de plugins.
Como testar dispatchers?
Teste cada função de estratégia isoladamente, depois um teste de integração por chave/tipo através do dispatcher.
O `singledispatch` funciona com ABCs?
Sim. O registro em classes base se aplica a subclasses, a menos que exista um registro mais específico.
E quanto aos manipuladores assíncronos?
Mantenha estratégias assíncronas como funções assíncronas; o dispatcher deve await nelas. Considere registradores assíncronos separados.
O `match/case` é um substituto?
Para variantes estáticas e conhecidas, match é legível. Para conjuntos abertos de plugins carregados de pontos de entrada, os registradores de dicionário continuam sendo melhores.
Como documentar chaves válidas?
Use tipos Literal, enums ou exponha CHANNELS.keys() na documentação da API e validação.
Estratégias podem ser lambdas?
Sim, para linhas únicas. Nomeie funções quando a lógica crescer - rastros de pilha e testes precisam de nomes legíveis.
E quanto aos fallbacks padrão?
.get(key, default_fn) para dicionários. Para singledispatch, mantenha uma implementação base sensata ou TypeError explícito.
Como isso se relaciona com o padrão OOP Strategy?
Dicionário de chamáveis é o padrão Strategy funcional - mesmo intenção, menos boilerplate de classe em Python.
Relacionados
- Padrões Factory & Builder - construção de objetos de estratégia
- Observer & Pub/Sub - dispatch de eventos
- Injeção de Dependência em Python - injetar estratégias
- Antipadrões Comuns - proliferação de
elif
Versões da Stack: Esta página foi escrita para Python 3.14.0 (estável 3.14, manutenção 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+ e uv 0.6+.