Design Patterns — padrões de projeto — são soluções reutilizáveis para problemas recorrentes no desenvolvimento de software. Foram catalogados pelo famoso livro do "Gang of Four" em 1994 e continuam relevantes. Em Python, muitos padrões são mais simples de implementar do que em Java ou C++ graças à tipagem dinâmica, funções de primeira classe e recursos como decoradores e metaclasses. Neste artigo cobrimos os padrões mais úteis no dia a dia, com implementações pythônicas.
Categorias de Padrões
Criacionais Estruturais Comportamentais
───────────────── ───────────────── ─────────────────
Singleton Adapter Strategy
Factory Method Decorator Observer
Abstract Factory Facade Command
Builder Composite Template Method
Prototype Proxy Iterator
Chain of Responsibility
State
Padrões Criacionais
Singleton
Garante que uma classe tenha apenas uma instância:
from threading import Lock
from typing import ClassVar
class Singleton:
"""Singleton thread-safe."""
_instancia: ClassVar["Singleton | None"] = None
_lock: ClassVar[Lock] = Lock()
def __new__(cls):
if cls._instancia is None:
with cls._lock:
if cls._instancia is None: # double-checked locking
cls._instancia = super().__new__(cls)
return cls._instancia
# Versão com decorator — curta, mas a classe vira uma função
def singleton(cls):
instancias = {}
def obter_instancia(*args, **kwargs):
if cls not in instancias:
instancias[cls] = cls(*args, **kwargs)
return instancias[cls]
return obter_instancia
@singleton
class ConfiguracaoApp:
def __init__(self):
self.debug = False
self.versao = "1.0.0"
self.database = "postgresql://localhost/escola"
c1 = ConfiguracaoApp()
c2 = ConfiguracaoApp()
print(c1 is c2) # True — mesma instância
O decorador tem um custo que costuma passar despercebido: depois dele, o nome ConfiguracaoApp aponta para a função obter_instancia, não mais para a classe. Medido: isinstance(c1, ConfiguracaoApp) levanta TypeError: isinstance() arg 2 must be a type, e herdar de ConfiguracaoApp deixa de ser possível. Em Python, o singleton mais simples é o próprio módulo: ele é importado uma vez só, e uma instância criada no nível do módulo (config = ConfiguracaoApp()) é compartilhada por quem fizer from app.config import config. Em testes, qualquer singleton é estado global que passa de um teste para o outro, e por isso vale oferecer um jeito de recriá-lo.
Factory Method
Define uma interface para criar objetos, deixando subclasses decidirem qual classe instanciar:
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class Relatorio(ABC):
titulo: str
conteudo: str
def exibir(self):
print(f"[{self.__class__.__name__}] {self.titulo}")
@abstractmethod
def exportar(self) -> bytes:
"""Todo formato exporta bytes, prontos para gravar ou enviar."""
class RelatorioPDF(Relatorio):
def exportar(self) -> bytes:
return f"PDF: {self.conteudo}".encode()
class RelatorioExcel(Relatorio):
def exportar(self) -> bytes:
return f"XLSX: {self.conteudo}".encode()
class RelatorioHTML(Relatorio):
def exportar(self) -> bytes:
return f"<html><body>{self.conteudo}</body></html>".encode()
class FabricaRelatorios:
"""Factory simples baseada em dicionário."""
_tipos = {
"pdf": RelatorioPDF,
"excel": RelatorioExcel,
"html": RelatorioHTML,
}
@classmethod
def criar(cls, tipo: str, titulo: str, conteudo: str) -> Relatorio:
classe = cls._tipos.get(tipo.lower())
if not classe:
raise ValueError(f"Tipo desconhecido: {tipo}. "
f"Opções: {list(cls._tipos)}")
return classe(titulo=titulo, conteudo=conteudo)
@classmethod
def registrar(cls, tipo: str, classe):
"""Extensível sem modificar a factory."""
cls._tipos[tipo] = classe
# Uso
rel = FabricaRelatorios.criar("pdf", "Boletim Março", "Conteúdo...")
rel.exibir()
# Registrando novo tipo sem modificar a classe
class RelatorioCSV(Relatorio):
def exportar(self) -> bytes:
return self.conteudo.encode()
FabricaRelatorios.registrar("csv", RelatorioCSV)
rel_csv = FabricaRelatorios.criar("csv", "Dados", "a,b,c")
print(rel_csv.exportar()) # b'a,b,c'
A primeira versão deste exemplo não declarava exportar na classe base, e dois defeitos passavam sem aviso: o relatório HTML devolvia str enquanto os outros devolviam bytes, e o RelatorioCSV registrado não tinha exportar nenhum. Medido: a fábrica o criava normalmente, e o erro só aparecia no uso, com AttributeError: 'RelatorioCSV' object has no attribute 'exportar'. Com o método abstrato, uma subclasse incompleta falha já na criação, com uma mensagem que diz o que falta. É o contrato que torna a fábrica extensível de verdade: quem registra um tipo novo sabe o que precisa implementar.
Builder
Constrói objetos complexos passo a passo:
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Consulta:
tabela: str
campos: list[str] = field(default_factory=lambda: ["*"])
condicoes: list[str] = field(default_factory=list)
ordenacao: Optional[str] = None
limite: Optional[int] = None
offset: int = 0
def to_sql(self) -> str:
campos_str = ", ".join(self.campos)
sql = f"SELECT {campos_str} FROM {self.tabela}"
if self.condicoes:
sql += " WHERE " + " AND ".join(self.condicoes)
if self.ordenacao:
sql += f" ORDER BY {self.ordenacao}"
if self.limite:
sql += f" LIMIT {self.limite}"
if self.offset:
sql += f" OFFSET {self.offset}"
return sql
class ConstrutorConsulta:
"""Builder fluente para consultas SQL."""
def __init__(self, tabela: str):
self._consulta = Consulta(tabela=tabela)
def selecionar(self, *campos: str) -> "ConstrutorConsulta":
self._consulta.campos = list(campos)
return self
def onde(self, condicao: str) -> "ConstrutorConsulta":
self._consulta.condicoes.append(condicao)
return self
def ordenar_por(self, campo: str, direcao: str = "ASC") -> "ConstrutorConsulta":
self._consulta.ordenacao = f"{campo} {direcao}"
return self
def limitar(self, n: int) -> "ConstrutorConsulta":
self._consulta.limite = n
return self
def paginar(self, pagina: int, tamanho: int = 10) -> "ConstrutorConsulta":
self._consulta.limite = tamanho
self._consulta.offset = (pagina - 1) * tamanho
return self
def construir(self) -> Consulta:
return self._consulta
# Interface fluente — method chaining
sql = (
ConstrutorConsulta("alunos")
.selecionar("id", "nome", "nota")
.onde("ativo = true")
.onde("nota >= 6.0")
.ordenar_por("nota", "DESC")
.paginar(pagina=2, tamanho=10)
.construir()
.to_sql()
)
print(sql)
# SELECT id, nome, nota FROM alunos WHERE ativo = true
# AND nota >= 6.0 ORDER BY nota DESC LIMIT 10 OFFSET 10
Padrões Estruturais
Adapter
Converte a interface de uma classe em outra que o cliente espera:
from abc import ABC, abstractmethod
class ProcessadorPagamento(ABC):
@abstractmethod
def processar(self, valor: float, descricao: str) -> dict:
...
# API do Stripe — interface que não controlamos
class StripeAPI:
def charge(self, amount_cents: int, currency: str, description: str) -> dict:
return {
"id": "ch_123",
"status": "succeeded",
"amount": amount_cents,
}
# API do PayPal — outra interface
class PayPalAPI:
def create_payment(self, total: float, currency: str, note: str) -> dict:
return {
"paymentId": "PAY-456",
"state": "approved",
"total": total
}
# Adapters — fazem as APIs externas conformarem à nossa interface
class AdapterStripe(ProcessadorPagamento):
def __init__(self, api: StripeAPI):
self._api = api
def processar(self, valor: float, descricao: str) -> dict:
centavos = round(valor * 100) # int() trunca: 19.99 * 100 = 1998.999…
resultado = self._api.charge(centavos, "BRL", descricao)
return {
"sucesso": resultado["status"] == "succeeded",
"id": resultado["id"],
"valor": resultado["amount"] / 100,
"gateway": "stripe"
}
class AdapterPayPal(ProcessadorPagamento):
def __init__(self, api: PayPalAPI):
self._api = api
def processar(self, valor: float, descricao: str) -> dict:
resultado = self._api.create_payment(valor, "BRL", descricao)
return {
"sucesso": resultado["state"] == "approved",
"id": resultado["paymentId"],
"valor": resultado["total"],
"gateway": "paypal"
}
def cobrar(processador: ProcessadorPagamento, valor: float, desc: str):
"""Funciona com qualquer gateway via Adapter."""
resultado = processador.processar(valor, desc)
status = "✓" if resultado["sucesso"] else "✗"
print(f"{status} [{resultado['gateway']}] R${resultado['valor']:.2f} — {desc}")
cobrar(AdapterStripe(StripeAPI()), 150.00, "Mensalidade Março")
cobrar(AdapterPayPal(PayPalAPI()), 150.00, "Mensalidade Março")
A conversão para centavos merece atenção, porque é o tipo de linha que ninguém revisa. A primeira versão fazia int(valor * 100), e o int() trunca em vez de arredondar. Em ponto flutuante, 19.99 * 100 vale 1998.9999999999998. Medido: int() dá 1998 centavos para R$ 19,99, 28 para R$ 0,29 e 114 para R$ 1,15. Com R$ 150,00, o valor do exemplo, o erro não aparece. round() dá o valor certo nos três casos, e o melhor, num sistema de pagamentos, é nem passar por float: guardar valores como Decimal ou como inteiro de centavos desde a entrada.
Facade
Fornece interface simplificada para um subsistema complexo:
class SubsistemaEmail:
def conectar(self, host: str): ...
def autenticar(self, user: str, senha: str): ...
def enviar(self, para: str, assunto: str, corpo: str): ...
def desconectar(self): ...
class SubsistemaTemplate:
def carregar(self, nome: str) -> str: ...
def renderizar(self, template: str, contexto: dict) -> str: ...
class SubsistemaFila:
def enfileirar(self, tarefa: dict): ...
def processar(self): ...
class FacadeNotificacao:
"""
Facade — simplifica envio de e-mails com template e fila.
O cliente não precisa conhecer os subsistemas internos.
"""
def __init__(self):
self._email = SubsistemaEmail()
self._template = SubsistemaTemplate()
self._fila = SubsistemaFila()
def enviar_boas_vindas(self, email: str, nome: str):
self._fila.enfileirar({
"tipo": "email",
"template": "boas_vindas",
"para": email,
"contexto": {"nome": nome}
})
def enviar_boletim(self, email: str, aluno: dict):
template = self._template.carregar("boletim")
corpo = self._template.renderizar(template, aluno)
self._email.conectar("smtp.gmail.com")
self._email.autenticar("escola@email.com", "senha")
self._email.enviar(email, "Seu Boletim", corpo)
self._email.desconectar()
# Interface simples para o cliente
notif = FacadeNotificacao()
notif.enviar_boas_vindas("ana@email.com", "Ana")
Padrões Comportamentais
Strategy
Define uma família de algoritmos intercambiáveis:
from abc import ABC, abstractmethod
from typing import Protocol
class EstrategiaOrdenacao(Protocol):
def ordenar(self, alunos: list) -> list: ...
class OrdenarPorNome:
def ordenar(self, alunos: list) -> list:
return sorted(alunos, key=lambda a: a["nome"])
class OrdenarPorNota:
def ordenar(self, alunos: list) -> list:
return sorted(alunos, key=lambda a: a["nota"], reverse=True)
class OrdenarPorFaltas:
def ordenar(self, alunos: list) -> list:
return sorted(alunos, key=lambda a: a["faltas"])
class GerenciadorTurma:
def __init__(self, estrategia: EstrategiaOrdenacao):
self._estrategia = estrategia
def definir_estrategia(self, estrategia: EstrategiaOrdenacao):
self._estrategia = estrategia
def listar(self, alunos: list) -> list:
return self._estrategia.ordenar(alunos)
alunos = [
{"nome": "Carla", "nota": 8.5, "faltas": 2},
{"nome": "Ana", "nota": 9.5, "faltas": 0},
{"nome": "Bruno", "nota": 7.0, "faltas": 5},
]
gerenciador = GerenciadorTurma(OrdenarPorNota())
print("Por nota:", [a["nome"] for a in gerenciador.listar(alunos)])
gerenciador.definir_estrategia(OrdenarPorNome())
print("Por nome:", [a["nome"] for a in gerenciador.listar(alunos)])
# Versão funcional com Python — Strategy como função
def listar_com_criterio(alunos: list, criterio) -> list:
return sorted(alunos, key=criterio)
print("Lambda:", [a["nome"] for a in
listar_com_criterio(alunos, lambda a: -a["nota"])])
Observer
Define dependência um-para-muitos — quando um objeto muda, todos os dependentes são notificados:
from abc import ABC, abstractmethod
from typing import Any
from dataclasses import dataclass
from dataclasses import field
from datetime import datetime, timezone
@dataclass
class Evento:
tipo: str
dados: dict
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
class Observador(ABC):
@abstractmethod
def atualizar(self, evento: Evento) -> None: ...
class SistemaEventos:
"""Event bus simples — publisher/subscriber."""
def __init__(self):
self._ouvintes: dict[str, list[Observador]] = {}
def inscrever(self, tipo_evento: str, observador: Observador):
self._ouvintes.setdefault(tipo_evento, []).append(observador)
def desinscrever(self, tipo_evento: str, observador: Observador):
if tipo_evento in self._ouvintes:
# lista não tem discard (isso é de set): remove() e ignora a ausência
try:
self._ouvintes[tipo_evento].remove(observador)
except ValueError:
pass
def publicar(self, evento: Evento):
for obs in self._ouvintes.get(evento.tipo, []):
try:
obs.atualizar(evento)
except Exception as e:
print(f"Erro no observador {obs}: {e}")
# Ouvintes de todos os eventos — com a mesma proteção
for obs in self._ouvintes.get("*", []):
try:
obs.atualizar(evento)
except Exception as e:
print(f"Erro no observador {obs}: {e}")
# Observadores concretos
class NotificadorEmail(Observador):
def atualizar(self, evento: Evento):
if evento.tipo == "aluno.aprovado":
print(f"[Email] Parabéns {evento.dados['nome']}! Você foi aprovado.")
class AuditoriaLogger(Observador):
def atualizar(self, evento: Evento):
print(f"[Auditoria] {evento.timestamp} | {evento.tipo} | {evento.dados}")
class AtualizadorPainel(Observador):
def atualizar(self, evento: Evento):
if evento.tipo in ["aluno.aprovado", "aluno.reprovado"]:
print(f"[Painel] Atualizando dashboard...")
# Uso
bus = SistemaEventos()
bus.inscrever("aluno.aprovado", NotificadorEmail())
bus.inscrever("aluno.aprovado", AuditoriaLogger())
bus.inscrever("aluno.reprovado", AuditoriaLogger())
bus.inscrever("*", AtualizadorPainel())
bus.publicar(Evento(
tipo="aluno.aprovado",
dados={"id": 1, "nome": "Ana Silva", "nota": 9.5}
))
bus.publicar(Evento(
tipo="aluno.reprovado",
dados={"id": 2, "nome": "Bruno Costa", "nota": 4.0}
))
Command
Encapsula uma requisição como objeto — permite desfazer, enfileirar e logar:
from abc import ABC, abstractmethod
from typing import Optional
from collections import deque
class Comando(ABC):
@abstractmethod
def executar(self) -> None: ...
@abstractmethod
def desfazer(self) -> None: ...
class ComandoAdicionarNota(Comando):
def __init__(self, aluno: dict, nota: float):
self.aluno = aluno
self.nota = nota
self._nota_anterior: Optional[float] = None
def executar(self) -> None:
self._nota_anterior = self.aluno.get("nota")
self.aluno["nota"] = self.nota
print(f"Nota {self.nota} adicionada para {self.aluno['nome']}")
def desfazer(self) -> None:
self.aluno["nota"] = self._nota_anterior
print(f"Nota desfeita para {self.aluno['nome']}: {self._nota_anterior}")
class ComandoDesativarAluno(Comando):
def __init__(self, aluno: dict):
self.aluno = aluno
def executar(self) -> None:
self.aluno["ativo"] = False
print(f"Aluno {self.aluno['nome']} desativado")
def desfazer(self) -> None:
self.aluno["ativo"] = True
print(f"Aluno {self.aluno['nome']} reativado")
class GerenciadorComandos:
"""Controla execução e histórico de comandos — suporta undo/redo."""
def __init__(self):
self._historico: deque[Comando] = deque(maxlen=50)
self._desfeitos: deque[Comando] = deque(maxlen=50)
def executar(self, comando: Comando) -> None:
comando.executar()
self._historico.append(comando)
self._desfeitos.clear()
def desfazer(self) -> None:
if not self._historico:
print("Nada para desfazer.")
return
comando = self._historico.pop()
comando.desfazer()
self._desfeitos.append(comando)
def refazer(self) -> None:
if not self._desfeitos:
print("Nada para refazer.")
return
comando = self._desfeitos.pop()
comando.executar()
self._historico.append(comando)
aluno = {"nome": "Ana", "nota": 7.0, "ativo": True}
gerenciador = GerenciadorComandos()
gerenciador.executar(ComandoAdicionarNota(aluno, 9.5))
gerenciador.executar(ComandoDesativarAluno(aluno))
print(f"Estado: {aluno}")
gerenciador.desfazer()
gerenciador.desfazer()
print(f"Após desfazer: {aluno}")
gerenciador.refazer()
print(f"Após refazer: {aluno}")
Chain of Responsibility
Passa uma requisição por uma cadeia de handlers:
from abc import ABC, abstractmethod
from typing import Optional
from dataclasses import dataclass
@dataclass
class Pedido:
valor: float
tipo: str
usuario_id: int
aprovado: bool = False
motivo: str = ""
class Handler(ABC):
def __init__(self):
self._proximo: Optional["Handler"] = None
def definir_proximo(self, handler: "Handler") -> "Handler":
self._proximo = handler
return handler
def processar(self, pedido: Pedido) -> Pedido:
if self._proximo:
return self._proximo.handle(pedido) # o PRÓXIMO elo decide
return pedido
@abstractmethod
def handle(self, pedido: Pedido) -> Optional[Pedido]: ...
class ValidacaoHandler(Handler):
def handle(self, pedido: Pedido) -> Optional[Pedido]:
if pedido.valor <= 0:
pedido.motivo = "Valor inválido"
pedido.aprovado = False
return pedido
return self.processar(pedido)
class LimiteHandler(Handler):
def __init__(self, limite: float):
super().__init__()
self.limite = limite
def handle(self, pedido: Pedido) -> Optional[Pedido]:
if pedido.valor > self.limite:
pedido.motivo = f"Valor acima do limite R${self.limite:.2f}"
pedido.aprovado = False
return pedido
return self.processar(pedido)
class FraudeHandler(Handler):
def handle(self, pedido: Pedido) -> Optional[Pedido]:
if pedido.usuario_id in [666, 999]:
pedido.motivo = "Usuário bloqueado por suspeita de fraude"
pedido.aprovado = False
return pedido
return self.processar(pedido)
class AprovacaoFinalHandler(Handler):
def handle(self, pedido: Pedido) -> Optional[Pedido]:
pedido.aprovado = True
pedido.motivo = "Aprovado"
return pedido
# Montando a cadeia
validacao = ValidacaoHandler()
limite = LimiteHandler(5000.0)
fraude = FraudeHandler()
aprovacao = AprovacaoFinalHandler()
validacao.definir_proximo(limite).definir_proximo(fraude).definir_proximo(aprovacao)
pedidos = [
Pedido(valor=250.00, tipo="compra", usuario_id=1),
Pedido(valor=-10.00, tipo="compra", usuario_id=2),
Pedido(valor=8000.00, tipo="saque", usuario_id=3),
Pedido(valor=100.00, tipo="compra", usuario_id=666),
]
for p in pedidos:
resultado = validacao.handle(p)
status = "✓" if resultado.aprovado else "✗"
print(f"{status} R${p.valor:>8.2f} | {resultado.motivo}")
Uma linha fazia toda a diferença nesse exemplo. Na primeira versão, o processar da classe base repassava o pedido chamando self._proximo.processar(pedido). Assim, o pedido pulava de elo em elo pelo método de repasse e nunca entrava no handle de nenhum deles. Medido: os quatro pedidos saíam recusados — o válido de R$ 250 inclusive —, e três deles com motivo vazio; o limite, a checagem de fraude e a aprovação final nunca rodavam. Com self._proximo.handle(pedido), cada elo decide e passa adiante. O defeito passava porque a saída "tudo recusado" parece o comportamento de um sistema rigoroso. Numa cadeia, vale um teste por elo: um pedido que só aquele elo deveria barrar.
Template Method
Define o esqueleto de um algoritmo, deixando subclasses preencherem os detalhes:
from abc import ABC, abstractmethod
class ProcessadorRelatorio(ABC):
"""Template Method — define o fluxo, subclasses definem os passos."""
def gerar(self, dados: list) -> str:
"""Método template — não deve ser sobrescrito."""
dados_limpos = self.limpar_dados(dados)
dados_proc = self.processar(dados_limpos)
conteudo = self.formatar(dados_proc)
cabecalho = self.cabecalho()
rodape = self.rodape(dados_proc)
return f"{cabecalho}\n{conteudo}\n{rodape}"
def limpar_dados(self, dados: list) -> list:
"""Hook — comportamento padrão pode ser sobrescrito."""
return [d for d in dados if d is not None]
@abstractmethod
def processar(self, dados: list) -> list: ...
@abstractmethod
def formatar(self, dados: list) -> str: ...
def cabecalho(self) -> str:
return "=== RELATÓRIO ==="
def rodape(self, dados: list) -> str:
return f"Total: {len(dados)} registros"
class RelatorioAprovados(ProcessadorRelatorio):
def processar(self, dados: list) -> list:
return [d for d in dados if d.get("nota", 0) >= 6.0]
def formatar(self, dados: list) -> str:
linhas = [f" ✓ {d['nome']:20} — {d['nota']:.1f}" for d in dados]
return "\n".join(linhas)
def rodape(self, dados: list) -> str:
return f"{len(dados)} aprovados listados acima."
class RelatorioRanking(ProcessadorRelatorio):
def processar(self, dados: list) -> list:
return sorted(dados, key=lambda d: d.get("nota", 0), reverse=True)
def formatar(self, dados: list) -> str:
linhas = [
f" {i:2}. {d['nome']:20} — {d['nota']:.1f}"
for i, d in enumerate(dados, 1)
]
return "\n".join(linhas)
def cabecalho(self) -> str:
return "=== RANKING DE NOTAS ==="
alunos = [
{"nome": "Ana", "nota": 9.5},
{"nome": "Bruno", "nota": 4.0},
{"nome": "Carla", "nota": 8.5},
{"nome": "Diego", "nota": 6.0},
]
print(RelatorioAprovados().gerar(alunos))
print()
print(RelatorioRanking().gerar(alunos))
Exemplo Completo: Sistema de Processamento de Pagamentos
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Protocol
from datetime import datetime, timezone
from collections import deque
import uuid
# ── Domínio ───────────────────────────────────────────
@dataclass
class Pagamento:
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
valor: float = 0.0
descricao: str = ""
gateway: str = ""
status: str = "pendente"
timestamp: datetime = field(
default_factory=lambda: datetime.now(timezone.utc)
)
tentativas: int = 0
erro: Optional[str] = None
# ── Observer: eventos do pagamento ────────────────────
class ObservadorPagamento(Protocol):
def on_pagamento(self, pagamento: Pagamento) -> None: ...
class LogPagamento:
def on_pagamento(self, p: Pagamento):
print(f" [Log] {p.status.upper()} | {p.id} | R${p.valor:.2f}")
class NotificadorPagamento:
def on_pagamento(self, p: Pagamento):
if p.status == "aprovado":
print(f" [Email] Pagamento de R${p.valor:.2f} aprovado!")
elif p.status == "recusado":
print(f" [Email] Pagamento recusado: {p.erro}")
# ── Strategy: gateways ────────────────────────────────
class GatewayPagamento(Protocol):
def cobrar(self, valor: float, descricao: str) -> dict: ...
class GatewaySimulado:
def __init__(self, nome: str, taxa_sucesso: float = 0.9):
self.nome = nome
self.taxa_sucesso = taxa_sucesso
import random
self._rand = random.Random(42)
def cobrar(self, valor: float, descricao: str) -> dict:
sucesso = self._rand.random() < self.taxa_sucesso
return {
"sucesso": sucesso,
"id_externo": f"{self.nome[:3].upper()}-{uuid.uuid4().hex[:6]}",
"erro": None if sucesso else "Cartão recusado"
}
# ── Chain: validações ─────────────────────────────────
class ValidadorPagamento(ABC):
def __init__(self):
self._proximo: Optional["ValidadorPagamento"] = None
def proximo(self, v: "ValidadorPagamento") -> "ValidadorPagamento":
self._proximo = v
return v
def validar(self, p: Pagamento) -> bool:
resultado = self._checar(p)
if resultado and self._proximo:
return self._proximo.validar(p)
return resultado
@abstractmethod
def _checar(self, p: Pagamento) -> bool: ...
class ValidarValor(ValidadorPagamento):
def _checar(self, p: Pagamento) -> bool:
if p.valor <= 0:
p.status = "recusado"
p.erro = "Valor inválido"
return False
return True
class ValidarLimite(ValidadorPagamento):
def __init__(self, limite: float):
super().__init__()
self.limite = limite
def _checar(self, p: Pagamento) -> bool:
if p.valor > self.limite:
p.status = "recusado"
p.erro = f"Acima do limite R${self.limite:.2f}"
return False
return True
# ── Facade: orquestra tudo ────────────────────────────
class ProcessadorPagamentos:
def __init__(self, gateway: GatewayPagamento):
self._gateway = gateway
self._observadores: list = [LogPagamento(), NotificadorPagamento()]
self._historico: deque[Pagamento] = deque(maxlen=100)
# Cadeia de validação
v1 = ValidarValor()
v2 = ValidarLimite(10_000.0)
v1.proximo(v2)
self._validador = v1
def processar(self, valor: float, descricao: str) -> Pagamento:
p = Pagamento(valor=valor, descricao=descricao)
print(f"\nProcessando: R${valor:.2f} — {descricao}")
# Validação (Chain of Responsibility)
if not self._validador.validar(p):
self._notificar(p)
self._historico.append(p)
return p
# Cobrança (Strategy)
p.tentativas += 1
resultado = self._gateway.cobrar(valor, descricao)
if resultado["sucesso"]:
p.status = "aprovado"
p.gateway = resultado["id_externo"]
else:
p.status = "recusado"
p.erro = resultado["erro"]
# Notificação (Observer)
self._notificar(p)
self._historico.append(p)
return p
def _notificar(self, p: Pagamento):
for obs in self._observadores:
obs.on_pagamento(p)
def relatorio(self) -> dict:
aprovados = [p for p in self._historico if p.status == "aprovado"]
recusados = [p for p in self._historico if p.status == "recusado"]
return {
"total": len(self._historico),
"aprovados": len(aprovados),
"recusados": len(recusados),
"valor_total": sum(p.valor for p in aprovados),
}
# ── Executando ────────────────────────────────────────
gateway = GatewaySimulado("Stripe", taxa_sucesso=0.8)
processador = ProcessadorPagamentos(gateway)
pagamentos = [
(250.00, "Mensalidade Março"),
(-10.00, "Valor inválido"),
(15000.00, "Acima do limite"),
(89.90, "Material didático"),
(500.00, "Matrícula"),
]
for valor, desc in pagamentos:
processador.processar(valor, desc)
print("\n=== Relatório ===")
rel = processador.relatorio()
for k, v in rel.items():
print(f" {k:15}: {v}")
Os padrões de projeto dão nome a soluções que se repetem, e em Python várias delas encolhem. O Singleton pode ser simplesmente um módulo; a Factory, um dicionário de classes; a Strategy, uma função passada como argumento. Outros continuam valendo a estrutura inteira: o Adapter isola APIs externas atrás de uma interface própria, o Observer desacopla quem publica de quem reage, o Command transforma operações em objetos que se podem desfazer, a Chain of Responsibility encadeia validações, e o Template Method fixa o esqueleto de um algoritmo e deixa os detalhes para as subclasses.
O que os defeitos desta página ensinam é que um padrão bem desenhado no diagrama pode estar quebrado no código, e ninguém percebe. A cadeia que recusava todo pedido parecia rigorosa; o rodapé que contava sempre zero parecia um detalhe; o desinscrever nunca tinha sido chamado. Anotações de tipo conferidas pelo mypy, como em Qualidade de Código: linting, type hints e mypy, e um teste por comportamento, como em Testes com pytest, pegam esse tipo de erro. O padrão organiza o código; quem garante que ele funciona são as verificações.
Fontes e leituras recomendadas
- Refactoring Guru — Padrões de Projeto — https://refactoring.guru/pt-br/design-patterns
- Python Patterns — repositório — https://github.com/faif/python-patterns
- GAMMA, Erich et al. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994. — o livro original do Gang of Four.
- PHILLIPS, Dusty; LOTT, Steven F. Python Object-Oriented Programming. 4. ed. Packt, 2021. Cap. 10–12 — design patterns com exemplos modernos em Python.
- PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. — Repository, Unit of Work, Event-Driven Architecture em Python.
Exercícios
Exercício 1
A cadeia de aprovação de pedidos está em produção, e o time comemora: nenhuma fraude passou em um mês. Um analista percebe outra coisa: nenhum pedido foi aprovado nesse mês, nem os de R$ 50. Na primeira versão do artigo, o processar da classe base fazia return self._proximo.processar(pedido). Por que isso bloqueava tudo, e por que o bloqueio passou por rigor?
Ver resposta
✓ Resposta: Porque o repasse chamava o método de repasse do próximo elo, e não o de decisão. O pedido entrava no handle do primeiro elo, a validação, que chamava self.processar. Dali ele pulava para o processar do segundo, que pulava para o do terceiro, e assim até o último, que não tinha próximo e devolvia o pedido do jeito que chegou: aprovado=False e motivo vazio. Nenhum handle depois do primeiro rodava. Medido com os quatro pedidos do artigo: todos recusados, três com motivo em branco, incluindo o de R$ 250 que deveria passar. A correção é self._proximo.handle(pedido). Passou por rigor porque "recusado" é o resultado seguro de um sistema de aprovação, e ninguém desconfia de um filtro que filtra demais. Duas práticas teriam pegado o erro: um teste do caminho feliz, em que um pedido válido precisa sair aprovado, e o motivo vazio tratado como erro, porque toda decisão de uma cadeia deveria dizer quem a tomou.
Exercício 2
O adapter do Stripe converte reais em centavos com int(valor * 100). Os testes, todos com R$ 150,00, passam. Em produção, uma cobrança de R$ 19,99 sai como 1998 centavos. Explique o centavo perdido e diga que outros valores sofrem do mesmo problema.
Ver resposta
✓ Resposta: O float não representa 19,99 exatamente; o valor guardado é um pouco menor, e 19.99 * 100 dá 1998.9999999999998. O int() não arredonda, ele trunca em direção a zero, e 1998,999… vira 1998. Medido: o mesmo acontece com R$ 0,29, que vira 28 centavos, e R$ 1,15, que vira 114. Com R$ 150,00 a multiplicação é exata, e o teste não tinha como pegar o erro. round(valor * 100) corrige os três casos. A correção de fundo é não usar float para dinheiro: receber o valor como texto e convertê-lo com Decimal("19.99"), ou trabalhar com inteiros de centavos desde a entrada. A lição de teste vale igualmente: valores "redondos" escondem erros de arredondamento. Um teste com uma lista de preços reais, como 19,99, 0,29 e 1,15, pega o problema na primeira execução.
Exercício 3
No Template Method dos relatórios, a classe base define rodape() retornando f"Total: {0} registros", e o ranking de quatro alunos termina com "Total: 0 registros". Por que o rodapé não tinha como acertar, e o que isso diz sobre o desenho de ganchos num Template Method?
Ver resposta
✓ Resposta: O {0} dentro da f-string é a expressão 0, uma constante, e não uma referência a algum dado. Mesmo que alguém quisesse contar, o método não tinha acesso aos registros: a assinatura era rodape(self), sem os dados processados. Num Template Method, os ganchos só podem usar o que o método-template lhes passa ou o que está guardado no objeto. Aqui, o gerar calculava dados_proc e não o repassava. A correção muda a assinatura para rodape(self, dados) e faz o gerar chamar self.rodape(dados_proc). Medido: o ranking passa a terminar com "Total: 4 registros", e o relatório de aprovados pode dizer "3 aprovados listados acima". A regra de desenho é pensar nos ganchos a partir do que uma subclasse razoável precisaria saber, e passar isso explicitamente. Mudar a assinatura de um gancho depois obriga a mudar todas as subclasses.
Exercício 4
Um colega usa o singleton por decorador do artigo e escreve, num teste, assert isinstance(config, ConfiguracaoApp). O teste falha com TypeError: isinstance() arg 2 must be a type. Depois, ao tentar criar class ConfiguracaoTeste(ConfiguracaoApp), recebe outro TypeError. O que o decorador fez com a classe?
Ver resposta
✓ Resposta: Substituiu a classe por uma função. @singleton equivale a ConfiguracaoApp = singleton(ConfiguracaoApp), e o singleton devolve obter_instancia, uma função que guarda a classe original numa closure. A partir daí, o nome ConfiguracaoApp é essa função: medido, type(ConfiguracaoApp).__name__ é "function". Por isso o isinstance recusa o segundo argumento e o class ... (ConfiguracaoApp) falha. Um decorador de classe que devolve outra coisa muda o que o nome significa para todo o resto do programa, incluindo anotações de tipo e ferramentas como o mypy. Para um singleton em Python, a saída mais simples é uma instância no nível do módulo. Se for preciso controlar a criação dentro da classe, o __new__ com trava, como na classe Singleton do começo do exemplo, mantém a classe como classe.
Exercício 5
O barramento de eventos funciona há meses. Um dia, uma tela precisa parar de receber notificações ao ser fechada, e a chamada bus.desinscrever("aluno.aprovado", painel) falha com AttributeError: 'list' object has no attribute 'discard'. O método existia desde o começo. Como o erro sobreviveu tanto tempo, e que ferramenta o teria pegado sem executar nada?
Ver resposta
✓ Resposta: Sobreviveu porque nunca foi executado: nenhum teste e nenhum caminho do sistema chamava desinscrever até aquele dia, e o Python só resolve .discard na hora em que a linha roda. O discard é de set; a lista tem remove, que levanta ValueError quando o item não está lá, daí o try na versão corrigida. O mypy teria pegado o erro sem executar nada. Os ouvintes estão anotados como dict[str, list[Observador]], e rodando o mypy no bloco original, sai error: "list[Observador]" has no attribute "discard" [attr-defined], junto com outro aviso real: o campo timestamp: datetime = None, que não aceita None pelo tipo declarado. É o argumento mais forte para anotar tipos: eles verificam também o código que os testes esquecem. Trocar a lista por um set também seria possível, mas mudaria o comportamento: os observadores deixariam de ser chamados na ordem de inscrição.