DEV Community

Cover image for Como e quando usar Django Signals
Lucas Mateus
Lucas Mateus

Posted on

Como e quando usar Django Signals

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)