[翻译]Django反模式:Signals
源文章: https://lincolnloop.com/blog/django-anti-patterns-signals/
Django的signal系统是一个很强的功能,但是其实很难用。并不是说它不能使用,只是它的使用场景比你想象的还要少。
首先,让我们来解释signal一个被很多人误解的知识:它不是异步执行的。没错,Django并不会在后台单独起一个worker线程来执行它们。
然后我们看一下下面的代码:
# 文件: models.py
from django.db import models
class Pizza(models.Model):
has_pepperoni = models.BooleanField(default=False)
class ToppingSales(models.Model):
name = models.CharField(max_length=100, unique=True)
units_sold = models.PositiveIntegerField(default=0)
#文件: signals.py
# File: signals.py
from django.db.models.signals import post_save
from django.db.models import F
from django.dispatch import receiver
from .models import Pizza
def pizza_saved_handler(sender, instance, created, **kwargs):
if created and instance.has_pepperoni:
ToppingSales.objects.filter(name='pepperoni').update(
units_sold=F('units_sold') + 1)
#文件: apps.py
from django.apps import AppConfig
from django.db.models.signals import post_save
class PizzeriaConfig(AppConfig):
def ready(self):
from .models import Pizza
from .signals import pizza_saved_handler
post_save.connect(pizza_saved_handler, sender=Pizza)
对比下面没有使用signal的例子:
# 文件: models.py
class Pizza(models.Model):
has_pepperoni = models.BooleanField(default=False)
def save(self, *args, **kwargs):
created = self.pk is None
super(Pizza, self).save(*args, **kwargs)
if created and self.has_pepperoni:
ToppingSales.objects.filter(name='pepperoni').update(
units_sold=F('units_sold') + 1)
class ToppingSales(models.Model):
name = models.CharField(max_length=100, unique=True)
units_sold = models.PositiveIntegerField(default=0)
这是一个人为制造的案例,但是它能说明一些东西。
使用signal的时候,我们需要把逻辑散步到3个文件。更糟糕的情况是,有时候signal并不总是放在signals.py文件,尤其是一些旧代码库。甚至你想找到这些signal都很麻烦很困难。
没有使用signal的例子,并不只是代码更少了,而且更加易读,更加容易测试。你可以通过阅读Pizza这个model的源代码,就能得知创建它所带来的副作用。
但是,将不同的逻辑解耦,把代码打散将不也是一个好事情吗?我同意,但是不应该以牺牲可读性为代价。上面的代码,我们不一定要把所有的代码都写在save()方法中,而只需要留一些“面包屑”,给将来阅读代码的人一些线索。
class Pizza(models.Model):
has_pepperoni = models.BooleanField(default=False)
def _update_toppings(self, created=False):
if created and self.has_pepperoni:
ToppingSales.objects.filter(name='pepperoni').update(
units_sold=F('units_sold') + 1)
def save(self, *args, **kwargs):
created = self.pk is None
super(Pizza, self).save(*args, **kwargs)
self._update_toppings(created)
通过重载你的save()和delete()方法,你可以完成几乎所有{pre, post}_{save, delete} signal可以做的事情,并且让代码更加易读。
同样的,request_{started, finished} 这些信号也可以通过中间件来完成, 在Django1.11中,新的LoginClassView, 已经可以通过继承的方式替换掉django.contrib.auth的信号了.
那么我们永远也不要使用signal吗?
也不一定。在一些有限的场景,signal还是有些价值的:
- 扩展第三方库的功能。如果你使用一个包含model的app,而且你无法控制它的代码,signal是一个好办法来代替mokey-patch。
- delete signal可以作用于批量删除方法(即
queryset.delete()),有时很有用。不过因为SQL生成方式的不同,批量更新的方法并不能被signal作用到。 - 在你需要使用同一个signal作用域多个model的时候,可以减少代码重复。
- 避免循环依赖。
如果你必须使用signal,请再三考虑并确定你是否真的需要它,还是只是想耍个小聪明。请你为未来的自己或者将来维护你代码的人考虑一下。

浙公网安备 33010602011771号