Django代码解耦:信号的运用

在任何项目中,我们或多或少都需要一种能力,即: 当某个事件发生时,另一个对象也能够知晓此事

拿博客举个栗子。我希望有 用户在博客留言时,博主收到通知 。按照比较容易想到的方式,那就是在保存评论数据时,显式执行发送通知相关的代码。

比如下面这样:

from django.db import models
from somewhere import post_notification
class Comment(models.Model):
    # ...
    def save(self, *args, **kwargs):
        # 发送通知
        post_notification()
       # ...

这样做的缺点就在于把 评论模块 通知模块 耦合到一起了。如果哪天我改动甚至删除了 通知模块 的对应函数,搞不好 评论模块 也无法正常工作了。

很多模块都关心评论模块的保存事件 时,代码就有可能变成了这样:

class Comment(models.Model):
    def save(self, *args, **kwargs):
        # 以下函数分属不同模块
        post_notification()
        save_log()
        increase_count()
        do_this()
        do_that()
        blablabla()
        do_this_again()
        do_that_again()
        blablabla_again()
        #...

模块之间还可以互相调用,搅在一起,动了其中一个可能就引出一堆报错,不利于功能的扩展。

信号的作用

因此,像这种 许多代码段对同一事件感兴趣时,信号就特别有用

Django 内置了对 信号 这个概念的支持。信号允许 发送器 通知 接收器 某些事件已经发生。当事件发生时,”发送器“只负责发出一个”信号“,提醒”接收器“该执行了;至于接收器具体是什么、有多少个,发送器就不关心了。

这就有点像村里的村长拿个大喇叭,站在村口喊:”长得帅的人该起床了!“至于到底哪些村民长得帅、喊出去的话有没有人听到、听到的到底起不起床,村长就完全不管了。

反过来讲,接收器在很多时候也并不关心到底是谁发出的信号,反正只要接收到唤醒自己的信号,直接执行就万事大吉了。

这种近似匿名的机制,再加之发送器、接收器都可以有多个,使得模块可以很轻松的解耦和功能扩展。

内置信号

Django 内置了几种常见的信号,开箱即用。

比如每当一个 HTTP 请求发起、结束时的信号:

from django.core.signals import request_finished, request_started
from django.dispatch import receiver
@receiver(request_finished)
def signal_callback(sender, **kwargs):
    print('信号已接收..')

上面的代码会在每个请求结束时执行。装饰器 @receiver 将函数标注为接收器,其参数 request_finished 指定了具体的信号类型。

request_finished 就是其中一个内置信号,在 http 请求结束时发送。

任何想成为接收器的函数必须包含下面两个参数:

  • sender 参数是发出信号的发送器。
  • **kwargs 关键字参数。之所以必须有它,是因为参数可能在任意时刻被添加到信号中,而接收器必须能够处理这些新的参数。

如前面说的,同一个信号的接收器可以有多个:

# from ...
@receiver(request_finished)
def signal_callback(sender, **kwargs):
    print('信号已接收1..')
@receiver(request_finished)
def signal_callback_2(sender, **kwargs):
    print('信号已接收2..')

同一个接收器的信号也可以有多个:

@receiver([request_finished, request_started])
def signal_callback(sender, **kwargs):
    print('信号已接收..')

有些时候你可能只对某一类信号中的 子集 感兴趣。比如说我只想在 BookModel 保存前触发接收器,而在 PersonModel 保存前不触发。于是你就可以这样做:

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import BookModel
@receiver(pre_save, sender=BookModel)
def my_handler(sender, **kwargs):
    # ...

装饰器中的 sender=BookModel 就表明了此接收器只响应 BookModel 的信号。

pre_save 对应的还有内置的 post_save 信号。

还有一个问题是:信号注册的代码有可能无意间被多次执行。为了防止重复注册导致的信号重复,可以给装饰器传递一个唯一的标识符,像这样:

@receiver(my_signal, dispatch_uid="my_unique_identifier")
def my_signal_handler(sender, **kwargs):
    # ...

标识符通常是字符串,但其实任何可散列的对象都可以。

以上就是内置信号的基础用法了。

更多内置信号,请见 Django内置信号

自定义信号

有时候内置信号可能无法满足需求,Django 也允许你自定义信号。下面用一个例子看看自定义信号是如何实现的。

假设我的项目中有一个叫 mySignal 的 App。新建 mySignal/signals.py 文件,注册一个自定义信号:

# mySignal/signals.py
import django.dispatch
# 注册信号
view_done = django.dispatch.Signal()

然后新建 mySignal/handlers.py ,编写接收器并把它和信号连接起来:

# mySignal/handlers.py
from django.dispatch import receiver
from mySignal.signals import view_done
@receiver(view_done, dispatch_uid="my_signal_receiver")
def my_signal_handler(sender, **kwargs):
    print(sender)
    print(kwargs.get('arg_1'), kwargs.get('arg_2'))

虽然已经有了信号和接收器,但是项目运行时并没有运行这两段代码。因此下面两步的作用是加载它们。

修改 mySignal/__init__.py

# mySignal/__init__.py
default_app_config = "mySignal.apps.MysignalConfig"

再修改 mySignal/apps.py

# mySignal/apps.py
from django.apps import AppConfig
class MysignalConfig(AppConfig):
    name = 'mySignal'
    def ready(self):
        import mySignal.handlers

差不多快完成了。接下来就可以在任意位置发送这个信号了。比如像这样:

# mySignal/views.py
from mySignal.signals import view_done
def some_view(request):
    # 发送信号
    view_done.send(
        sender='View function...',