返回介绍

建议52:用发布订阅模式实现松耦合

发布于 2024-01-30 22:19:09 字数 4180 浏览 0 评论 0 收藏 0

发布订阅模式(publish/subscribe或pub/sub)是一种编程模式,消息的发送者(发布者)不会发送其消息给特定的接收者(订阅者),而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。这种发布者和订阅者的解耦可以允许更好的可扩放性和更为动态的网络拓扑,故受到了大家的喜爱。

发布订阅模式的优点是发布者与订阅者松散的耦合,双方不需要知道对方的存在。由于主题是被关注的,发布者和订阅者可以对系统拓扑毫无所知。无论对方是否存在,发送者和订阅者都可以继续正常操作。要实现这个模式,就需要有一个中间代理人,在实现中一般被称为Broker,它维护着发布者和订阅者的关系:订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。简单的实现如下:

from collections import defaultdict
route_table = defaultdict(list)
def sub(self, topic, callback):
     if callback in route_table[topic]
        return 
     route_table[topic].append(callback)
def pub(self, topic, *a, **kw):
     for func in route_table[topic]:
        func(*a, **kw)

这个实现非常简单,直接放在一个叫Broker.py的模块中(这显然是单件),省去了各种参数检测、优先处理的需求等,甚至没有取消订阅的函数,但它的确展现了发布订阅模式实现的最基础的结构,它的应用代码也可以运行。

import Broker
def greeting(name):
     print 'Hello, %s.'%name
Broker.sub('greet', greeting)
Broker.pub('greet', 'LaiYonghao')
# 
输出
Hello, LiaYonghao.

相对于这个简化版本,blinker和python-message两个模块的实现要完备得多。blinker已经被用在了多个广受欢迎的项目上,比如flask和django;而python-message则支持更多丰富的特性。本节以python-message的使用为例,讲解发布订阅模式的应用场景。

安装python-message相当简单,通过pip安装就可以了。

pip install message

然后简单验证一下。

import message
def hello(name):
  print 'hello, %s.'%name
message.sub('greet', hello)
message.pub('greet', 'lai')

运行输出如下:

hello, lai.

接下来用它解决一些实际问题。假定你给项目组开发了一个程序库foo,里面有一个非常重要的函数——bar。

def bar():
  print 'Haha, Calling bar().'
  do_sth()

这个函数如此重要,所以你给它加上了一行输出代码,用以输出日志。后来你的这个程序库foo被大量使用了,一直运行得很好,直到又一个新项目拖你过去“救火”,因为出了bug无法查出原因,怀疑是foo的问题。你查看了很久日志,都没有发现他们调用bar()的痕迹,一问,原来他们是用logging的,标准输出在做Daemon的时候被重定向到了/dev/null。在临时修改了输出重定向以后,找到了bug所在,并解决了。然后你开始着手解决这个问题。一开始你想在你的foo库中引入logging,但原来的项目又不用logging,你在程序库里引入logging,但谁来初始化它呢?就算你引入了logging,则你们的项目可能是用logging.getLogger('prjA')获取logger,另一个项目可能是用logging.getLogger('prjB'),日后还有新项目呢!一想到要兼容这么多项目你就头大了。忍痛割爱,把print语句给删除掉吧,又怕日后出了问题自己都找不到bug,那还不是自己加班自己苦。这个时候,不妨让python-message来帮你,轻松改一下bar()函数。

import message
LOG_MSG = ('log', 'foo')
def bar():
  message.pub(LOG_MSG, 'Haha, Calling bar().')
  do_sth()

在已有的项目中,只需要在项目开始处加上这样的代码,继续把日志放到标准输出。

import message
import foo
def handle_foo_log_msg(txt):
  print txt
message.sub(foo.LOG_MSG, handle_foo_log_msg)

而在那个使用logging的新项目中,则这样修改:

def handle_foo_log_msg(txt):
  import logging
  logging.debug(txt)

甚至在一些不关注底层库的日志项目中,直接无视就可以了。通过message,可以轻松获得库与应用之间的解耦,因为库关注的是要有日志,而不关注日志输出到哪里;应用关注的是日志要统一放置,但不关注谁往日志文件中输出内容,这正与发布订阅模式的应用场景不谋而合。

除了简单的sub()/pub()之外,python-message还支持取消订阅(unsub())和中止消息传递。

import message
def hello(name):
  print 'hello %s' % name
  ctx = message.Context()
  ctx.discontinued = True
  return ctx
def hi(name):
  print 'u cann\'t c me.'
message.sub('greet', hello)
message.sub('greet', hi)
message.pub('greet', 'lai')

python-message利用回调函数的返回值来实现取消消息传递,非常巧妙(读者可以思考一下为什么能够利用回调函数的返回值)。在上面这个例子中,运行后是看不到“u can't c me.”这一行输出的,因为消息在调用hello()后就中止传递了(Broker使用list对象存储回调函数就是为了保证次序)。

python-message是同步调用回调函数的,也就是说谁先sub谁就先被调用。大部分情况下这样已经能够满足大分需求,但有时需要后sub的函数先被调用,这时message.sub函数通过一个默认参数来支持的,只需要简单地在调用sub的时候加上front=True,这个回调函数将被插到所有之前已经sub的回调函数之前:sub('greet',hello,front=True)。

订阅/发布模式是观察者模式的超集,它不关注消息是谁发布的,也不关注消息由谁处理。但有时候我们也希望某个自己的类的也能够更方便地订阅/发布消息,也就是想退化为观察者模式,python-message同样提供了支持。如以下代码:

from message import observable
def greet(people):
     print 'hello, %s.'%people.name
@observable
class Foo(object):
     def __init__(self, name):
       print 'Foo'
       self.name = name
       self.sub('greet', greet)
     def pub_greet(self):
       self.pub('greet', self)
foo = Foo('lai')
foo.pub_greet()

python-message提供了类装饰函数observable(),任何class只需要通过它装饰一下就拥有了sub/unsub/pub/declare/retract等方法,它们的使用方法跟全局函数是类似的,在此不赘述。

注意

因为python-message的消息订阅默认是全局性的,所以有可能产生名字冲突。在减少名字冲突方面,可以借鉴java/actionscript3的package起名策略,比如在应用中定义消息主题常量FOO='com.googlecode.python-message.FOO',这样多个库同时定义FOO常量也不容易冲突。除此之外,还有一招就是使用uuid,如下:

  uuid = 'bd61825688d72b345ce07057b2555719'
  FOO = uuid + 'FOO'

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文