Django代码解耦:信号的运用
3730 views, 2021/04/29 updated Go to Comments
在任何项目中,我们或多或少都需要一种能力,即:当某个事件发生时,另一个对象也能够知晓此事。
拿博客举个栗子。我希望有用户在博客留言时,博主收到通知。按照比较容易想到的方式,那就是在保存评论数据时,显式执行发送通知相关的代码。
比如下面这样:
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...', arg_1='My signal...', arg_2='received...' ) # 其他代码...
重启服务器,访问此视图后,控制台将输出如下字符:
View function... My signal... received...
接收器成功唤醒了,并且正确接收到信号携带的信息。
总结
信号的优点是让模块之间解耦。当不同模块的代码片段对同一事物感兴趣时,就是信号非常适合的应用场景。信号的缺点是它是隐式执行的,这使得调试变得更困难。
事物都有两面性,是否使用信号还得根据实际需求进行判断。