O padrão Observer
Antes de abordarmos as signals no Django, vale entender o padrão de design que as inspira: o Observer.
O padrão Observer, também conhecido como "publicador-assinante" (ou pub-sub), consiste em definir um mecanismo onde certos objetos (os observadores) reagem automaticamente a mudanças em outro objeto (o sujeito). Isso é útil quando você quer que partes diferentes do sistema fiquem "de olho" em algum evento e ajam quando ele ocorrer, sem precisar acoplar diretamente os componentes envolvidos.
No contexto do Django, o sujeito geralmente é um model (por exemplo um User
) e os observadores são funções que você registra para reagir a eventos como salvar ou deletar esse modelo. Sempre que um desses eventos acontece, todos os observadores registrados são notificados automaticamente.
Esse padrão ajuda a manter responsabilidades separadas, evitando que toda a lógica fique concentrada na view, no serializer ou no próprio model. Mas como veremos mais adiante, ele também pode ser uma armadilha se usado sem critério.
Introdução
As signals no Django oferecem uma maneira de reagir a eventos internos do framework. Por exemplo, você pode executar uma ação sempre que um objeto for salvo, deletado ou tiver uma relação alterada. São muito uteis principalmente quando temos varias partes do código que precisam observar os mesmos eventos. A promessa de "desacoplamento" é muito atrativa: você consegue executar lógicas importantes sem poluir sua view ou serializer com responsabilidades extras.
No entanto, com o tempo, o uso indiscriminado de signals pode tornar seu código imprevisível, difícil de testar e ainda mais difícil de manter. Nessa publicação, vou mostrar como usar Django signals de forma clara e controlada, além de discutir em quais casos elas devem ser evitadas.
O que são Django Signals?
As signals permitem conectar funções a determinados eventos do ciclo de vida de um objeto. Por exemplo, o post_save
é executado depois que um objeto é salvo, e o pre_delete
é chamado antes de um objeto ser deletado.
Essas funções são chamadas automaticamente quando o evento ocorre. Isso é feito com a função connect
ou usando o decorador @receiver
.
Vejamos um exemplo simples: após salvar um novo usuário, um e-mail de boas-vindas é enviado.
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .utils import enviar_email_de_boas_vindas
@receiver(post_save, sender=User)
def enviar_email_apos_cadastro(sender, instance, created, **kwargs):
if created:
enviar_email_de_boas_vindas(instance.email)
O decorator @receiver
conecta essa função ao evento post_save
do modelo User
. Sempre que um novo usuário for criado, a função será executada automaticamente.
Exemplos práticos de uso
Outro uso comum de signals é a remoção de arquivos físicos relacionados a um model quando ele for deletado, pois como sabemos o Django por padrão salva os arquivos (fotos, pdfs e etc) em um diretório escolhido em disco. Suponha que você tenha um model Documento
com um campo arquivo
que armazena o caminho do arquivo em disco. Ao deletar esse objeto, você pode garantir que o arquivo também seja removido:
@receiver(pre_delete, sender=Documento)
def deletar_arquivo_fisico(sender, instance, **kwargs):
instance.arquivo.delete(save=False)
Também é possível usar signals para reagir a alterações em relações ManyToMany. No exemplo abaixo, quando alunos são adicionados a um curso, a aplicação registra o evento:
from django.db.models.signals import m2m_changed
@receiver(m2m_changed, sender=Curso.alunos.through)
def logar_mudancas_em_alunos(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
print(f"Alunos adicionados ao curso {instance.id}: {pk_set}")
Quando não usar signals
Apesar da utilidade, signals não são indicadas para toda e qualquer lógica. Um dos principais problemas é que elas introduzem comportamentos "ocultos" no sistema. Isso pode tornar o fluxo da aplicação difícil de entender, já que a lógica associada não está visível no lugar onde a ação original acontece.
Por exemplo, imagine que você cria um pedido (Order
) e, em algum lugar escondido, uma signal gera uma fatura. Se essa signal falhar silenciosamente ou não for executada, a fatura simplesmente não será criada. E o pior: você provavelmente nem vai notar.
Além disso, signals não garantem a ordem de execução entre si. Se você tem duas functions escutando o mesmo evento e espera que uma rode antes da outra, está assumindo algo que o Django não promete.
Outro problema comum é que signals não têm acesso direto ao contexto da requisição. Se você precisa saber quem é o usuário autenticado que disparou um evento, ou acessar dados da request, esse tipo de lógica não deve estar numa signal.
Por isso, lógica de negócio importante ou complexa não deve depender de signals. Nesses casos, é melhor usar métodos explícitos.
Alternativas mais claras
Uma abordagem mais controlada é encapsular a lógica em uma camada de serviço. Isso significa criar funções que executam ações de negócio de maneira previsível e que você chama diretamente da view, serializer ou command.
Por exemplo, ao invés de criar um usuário e deixar a signal cuidar do envio de e-mail, você pode fazer isso em uma função de serviço:
def criar_usuario_e_enviar_email(data):
user = User.objects.create(**data)
enviar_email_de_boas_vindas(user.email)
return user
Esse padrão torna a lógica mais clara, mais fácil de testar e reduz o acoplamento entre os componentes.
Outra opção é sobrescrever os métodos save()
ou delete()
do modelo. Isso é útil quando a lógica está diretamente relacionada à integridade do próprio objeto, como deletar um arquivo ao excluir um modelo:
def delete(self, *args, **kwargs):
self.arquivo.delete(save=False)
super().delete(*args, **kwargs)
Testando signals de forma segura
Signals podem dificultar a criação de testes, principalmente quando geram efeitos colaterais. Para contornar isso, você pode usar mock.patch
para simular funções chamadas pela signal, evitando que a lógica externa seja realmente executada durante o teste.
from unittest import mock
with mock.patch("app.signals.enviar_email_de_boas_vindas"):
response = client.post("/api/usuarios/", data)
Outra opção é desconectar a signal durante o teste:
from django.db.models.signals import post_save
post_save.disconnect(enviar_email_apos_cadastro, sender=User)
Ambas as técnicas ajudam a garantir que seus testes sejam confiáveis e não dependam de eventos implícitos.
Conclusão
Django Signals são úteis para acionar efeitos colaterais de forma automática e desacoplada. Mas sua aparente simplicidade pode esconder perigos: acoplamento oculto, falta de controle sobre o fluxo e dependência de eventos implícitos.
Use signals para tarefas secundárias, como logs, manipulação de arquivos ou notificações simples. Para lógica de negócio central, prefira abordagens explícitas, como uma camada de serviço.
Um bom critério é se perguntar: essa lógica precisa ser previsível, rastreável e testável? Se sim, não deve estar em uma signal.
Esse tipo de clareza é o que diferencia sistemas sustentáveis de sistemas frágeis. O uso consciente dessa ferramenta pode melhorar bastante a arquitetura da sua aplicação ou destruí-la silenciosamente.
Referências
Essa publicação foi baseada no que consta na própria documentação do Django que pode ser vista nesse link.
Você também pode querer aprender um pouco mais sobre o Observer, então fica aqui minha indicação de um conteudo bem simples e educativo a respeito desse e dos demais padrões de projeto, só dar uma olhadinha nesse link.
Top comments (0)