Django代码解耦:信号的运用

1034 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...

接收器成功唤醒了,并且正确接收到信号携带的信息。

总结

信号的优点是让模块之间解耦。当不同模块的代码片段对同一事物感兴趣时,就是信号非常适合的应用场景。信号的缺点是它是隐式执行的,这使得调试变得更困难。

事物都有两面性,是否使用信号还得根据实际需求进行判断。




本文作者: 杜赛
发布时间: 2021年04月29日 - 14:59
最后更新: 2021年04月29日 - 14:59
转载请保留原文链接及作者
本站使用 Github 评论后端。鉴于国内复杂的网络环境,如遇评论无法加载、发表等问题,请尝试刷新页面,或更换上网姿势。