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...',