[翻译]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还是有些价值的:

  1. 扩展第三方库的功能。如果你使用一个包含model的app,而且你无法控制它的代码,signal是一个好办法来代替mokey-patch。
  2. delete signal可以作用于批量删除方法(即queryset.delete()),有时很有用。不过因为SQL生成方式的不同,批量更新的方法并不能被signal作用到。
  3. 在你需要使用同一个signal作用域多个model的时候,可以减少代码重复。
  4. 避免循环依赖。

如果你必须使用signal,请再三考虑并确定你是否真的需要它,还是只是想耍个小聪明。请你为未来的自己或者将来维护你代码的人考虑一下。

posted @ 2019-02-25 22:47  thomaszdxsn  阅读(167)  评论(0)    收藏  举报