Django Signals : architecture découplée et event-driven

Les signaux Django permettent de réagir aux événements du framework sans coupler vos composants. Comprendre leur fonctionnement ouvre la porte à une architecture plus propre, plus testable et plus extensible.

Qu'est-ce qu'un Signal ?

Un signal est un mécanisme de notification qui permet à des composants découplés de communiquer. Quand un événement se produit (sauvegarde d'un modèle, requête HTTP, etc.), Django émet un signal. Les receivers connectés à ce signal sont alors exécutés.

Le pattern Observer

Les signaux implémentent le pattern Observer : un émetteur (sender) notifie des abonnés (receivers) sans les connaître directement. Cela permet d'ajouter des comportements sans modifier le code existant.

Django fournit des signaux built-in pour les événements courants : pre_save, post_save, pre_delete, post_delete, request_started, request_finished, et bien d'autres.

Signaux de modèle : les essentiels

post_save : réagir après la sauvegarde

Le signal le plus utilisé. Il est émis après qu'un modèle soit sauvegardé en base. Parfait pour déclencher des actions secondaires.

                
users/signals.py
from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth import get_user_model from .models import Profile from .tasks import send_welcome_email User = get_user_model() @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): """Crée un profil automatiquement pour chaque nouvel utilisateur.""" if created: Profile.objects.create(user=instance) # Envoyer l'email en tâche async send_welcome_email.delay(instance.id)

pre_save : valider ou modifier avant sauvegarde

                
products/signals.py
from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils.text import slugify from .models import Product @receiver(pre_save, sender=Product) def generate_slug(sender, instance, **kwargs): """Génère automatiquement le slug avant sauvegarde.""" if not instance.slug: instance.slug = slugify(instance.name) # Gérer les doublons counter = 1 original_slug = instance.slug while Product.objects.filter(slug=instance.slug).exists(): instance.slug = f"{original_slug}-{counter}" counter += 1

Attention aux boucles infinies

Appeler instance.save() dans un receiver post_save déclenche à nouveau le signal. Utilisez update_fields ou un flag pour éviter les boucles infinies.

Enregistrer les receivers correctement

Les receivers doivent être importés au démarrage de Django pour être connectés. La méthode recommandée : utiliser AppConfig.ready().

                
users/apps.py
from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' def ready(self): # Importer les signals pour les connecter import users.signals # noqa: F401

Structure recommandée

Créez un fichier signals.py par application. Cela garde le code organisé et facilite le debug. Évitez de mettre les receivers directement dans models.py.

Créer des signaux personnalisés

Pour des événements métier spécifiques, créez vos propres signaux. Cela découple la logique et permet à d'autres parties de l'application de réagir.

                
orders/signals.py
from django.dispatch import Signal # Définir les signaux personnalisés order_placed = Signal() # args: order, user order_paid = Signal() # args: order, payment order_shipped = Signal() # args: order, tracking_number

Émettre le signal

                
orders/services.py
from .signals import order_placed, order_paid class OrderService: def create_order(self, user, cart): order = Order.objects.create( user=user, total=cart.get_total() ) for item in cart.items.all(): OrderItem.objects.create( order=order, product=item.product, quantity=item.quantity ) # Émettre le signal order_placed.send( sender=self.__class__, order=order, user=user ) return order

Écouter le signal

                
notifications/receivers.py
from django.dispatch import receiver from orders.signals import order_placed, order_paid from .tasks import send_order_confirmation, notify_warehouse @receiver(order_placed) def handle_order_placed(sender, order, user, **kwargs): """Envoie la confirmation de commande.""" send_order_confirmation.delay(order.id) @receiver(order_paid) def handle_order_paid(sender, order, payment, **kwargs): """Notifie l'entrepôt pour préparer la commande.""" notify_warehouse.delay(order.id)

Bonnes pratiques

Garder les receivers légers

Un receiver doit exécuter une opération rapide ou déléguer à une tâche async. Ne pas bloquer le thread principal avec des opérations longues.

                
Mauvais vs Bon
# ❌ Mauvais : opération longue dans le receiver @receiver(post_save, sender=Order) def generate_invoice_pdf(sender, instance, **kwargs): pdf = render_to_pdf(instance) # Lent ! send_email(instance.user.email, pdf) # Encore plus lent ! # ✅ Bon : déléguer à Celery @receiver(post_save, sender=Order) def schedule_invoice(sender, instance, created, **kwargs): if created: generate_and_send_invoice.delay(instance.id)

Éviter les effets de bord cachés

Quand ne pas utiliser les signaux

Les signaux ne sont pas la solution à tout. Pour des opérations qui doivent toujours se produire ensemble, préférez un service explicite. Les signaux brillent pour les extensions et les intégrations optionnelles.

Tester les signaux

Les signaux peuvent rendre les tests complexes. Django fournit des outils pour les désactiver temporairement.

                
tests/test_orders.py
from django.test import TestCase from django.db.models.signals import post_save from unittest.mock import patch from orders.models import Order from orders.signals import order_placed class OrderTestCase(TestCase): def test_order_creation_emits_signal(self): """Vérifie que le signal est émis à la création.""" handler = Mock() order_placed.connect(handler) order = create_order(user=self.user, cart=self.cart) handler.assert_called_once() args, kwargs = handler.call_args self.assertEqual(kwargs['order'], order) order_placed.disconnect(handler) @patch('notifications.receivers.send_order_confirmation') def test_order_triggers_notification(self, mock_task): """Vérifie que la notification est déclenchée.""" order = create_order(user=self.user, cart=self.cart) mock_task.delay.assert_called_once_with(order.id)

Alternatives aux signaux

Les signaux ne sont pas toujours la meilleure solution. Voici des alternatives selon le contexte :

Les signaux sont un outil de découplage, pas une architecture. Utilisez-les pour permettre à des modules indépendants de communiquer, pas pour éviter d'organiser votre code. Un bon signal répond à la question : "Qui d'autre pourrait avoir besoin de savoir que cet événement s'est produit ?"

Ressources

Besoin d'aide sur votre projet ?

Je peux vous accompagner dans le développement ou l'optimisation de votre application.

Me contacter