前言
如果大家跟我一样,经常看文章的话,就会发现很多作者对观察者模式和发布订阅模式是进行区分的。基于相信权威的学习法,我们可以看看相关书籍是如何定义观察者模式与发布订阅模式的。
曾探著的《JavaScript设计模式与开发实践》一书中对发布订阅模式有以下描述:
发布---订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
还有一本美国人写的《JavaScript设计模式》一书中对观察者模式有以下描述:
在事件驱动的环境中,比如浏览器这种持续寻求用户关注的环境中,观察者模式(又名发布者-订阅者(publisher-subscriber)模式)是一种管理人与其任务之间的关系(确切地讲,是对象及其行为和状态之间的关系)的得力工具。
设计模式不单是前端独有,后端同样有相关的设计模式实践。在一本由韩敬海主编讲解Java的设计模式的书籍:《设计模式(Java版)》中也有对观察者模式的描述:
观察者模式(Observer Pattern)也称发布订阅模式,它是一种在项目中经常使用的模式。
所以所谓观察者模式就是发布订阅模式,它们只是不同的叫法。
发布订阅模式的作用与实现
现实中的发布订阅模式------公众号例子
我们现实生活中就存在非常多的发布订阅模式,比如我们微信上的公众号就是一个典型的发布订阅模式。当我们关注了公众号(也就是订阅)后,公众号只要发布文章,我们就可以收到文章信息了。在这里公众号就是发布者 或者叫被观察者 ,关注公众号的人就是订阅者 或者叫观察者。
我们可以通过 JavaScript 代码进行抽象模拟公众号的策略行为。
定义发布者公众号对象以及实现公众号拥有的添加订阅者、广播信息、取消订阅的功能
// 定义发布者公众号
const weChatOfficialAccount = {
// 订阅公众号的人的记录列表
subscribers: [],
// 添加订阅者
addDep(fn) {
// 把订阅者添加进记录列表
this.subscribers.push(fn)
},
// 广播信息
notify(title) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(fn => fn(title));
},
// 取消订阅
remove(fn) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(fn)
// 删除订阅者
this.subscribers.splice(index, 1 )
}
}
上述代码模拟了公众号拥有一些功能,接下来我们进行订阅者的模拟测试。
// 订阅者小明
const subscriber = (title) => {
console.log('小明收到的公众号文章:', title)
}
// 小明关注公众号
weChatOfficialAccount.addDep(subscriber)
上述代码我们定义了一个订阅者小明(可以理解为函数 subscriber),小明关注公众号,其实就是调用公众号对象的 addDep 方法将订阅者小明添加到公众号的关注者记录列表中。
模拟发布者公众号发布文章:
// 发布者公众号发布文章,小明将收到文章信息
weChatOfficialAccount.notify('依赖追踪的基础 ------ 发布订阅模式')
上述代码将打印:小明收到的公众号文章:依赖追踪的基础 ------ 发布订阅模式。
模拟取消关注
// 小明取消关注公众号
weChatOfficialAccount.remove(subscriber)
// 公众号再次发布文章,小明不再收到文章信息
weChatOfficialAccount.notify('新文章:依赖追踪的基础 ------ 发布订阅模式')
我们可以看到当取消关注公众号后,公众号再次发布文章,订阅者小明将不再打印相关信息。
通过上述代码我们实现了一个最简单的发布订阅模式。但上述小明角色和行为还不够清晰,我们更改一下小明的角色代码,让其更语义化些。
// 订阅者小明
const subscriber = {
// 关注公众号
follow() {
weChatOfficialAccount.addDep(subscriber)
},
// 取消关注
unfollow() {
weChatOfficialAccount.remove(subscriber)
},
// 接受公众号推送信息
update(title) {
console.log('小明收到的公众号文章:', title)
}
}
同样地我们让公众号发布文章这个动作也更语义化些。
const weChatOfficialAccount = {
// 文章内容
article: '',
// 发布文章
setArticle(value) {
this.article = value
// 更新文章的时候通知所有的订阅者
this.notify()
},
// 广播信息
notify() {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(obj => obj.update(this.article))
},
// ...
}
我们更改好的公众号对象多了一个文章内容的属性:article 和发布文章的方法 setArticle(),在更新文章的时候去通知所有的订阅者,并且通过订阅者对象上的 update 方法告诉订阅者更新了什么文章内容。
接下来小明订阅公众号的动作只需要执行自己的 follow 方法即可。
小明订阅公众号
// 小明订阅公众号
subscriber.follow()
公众号发布文章
// 公众号发布文章
weChatOfficialAccount.setArticle('什么是发布订阅模式?')
将打印:小明收到的公众号文章:什么是发布订阅模式?。
小结
通过上述代码例子,我们就可以很好的印证了文章开头对 发布订阅模式 的定义,即:当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知 。我们从上述例子中可以看到当公众号对象的文章属性 article 的状态发生改变的时候,依赖它的对象 ------ 订阅者小明便得到了通知,打印了更新的文章内容。
熟悉 Vue 数据响应式原理的同学肯定感觉到很亲切,在数据响应式当中,一个响应式对象的属性状态发生改变的时候就会去通知所有依赖该属性的副作用函数进行重新执行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义发布者公众号
const weChatOfficialAccount = {
// 文章内容
article: '',
// 发布文章
setArticle(value) {
this.article = value
// 更新文章的时候通知所有的订阅者
this.notify()
},
// 订阅公众号的人的记录列表
subscribers: [],
// 添加订阅者
addDep(fn) {
// 把订阅者添加进记录列表
this.subscribers.push(fn)
},
// 广播信息
notify() {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(obj => obj.update(this.article))
},
// 取消订阅
remove(fn) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(fn)
// 删除订阅者
this.subscribers.splice(index, 1)
}
}
// 订阅者小明
const subscriber = {
// 关注公众号
follow() {
weChatOfficialAccount.addDep(subscriber)
},
// 取消关注
unfollow() {
weChatOfficialAccount.remove(subscriber)
},
// 接受公众号推送信息
update(title) {
console.log('小明收到的公众号文章:', title)
}
}
// 小明订阅公众号
subscriber.follow()
// 公众号发布文章
weChatOfficialAccount.setArticle('什么是发布订阅模式?')
</script>
</html>
现实中的发布订阅模式------求职者与企业
在以前网络没那么发达的年代,大家可能是通过报纸、线下公告栏去找工作的,那么很有可能去找的时候,该公司已经不需要人了,但在那个工作机会缺乏的年代,你可能会留下联系方式给招聘公司,并告诉他如果什么时候缺人了就告诉你。同样地也有很多其他求职者也在招聘公司留下了联系方式,当招聘公司缺人了,就会通过记录表中的名单一一进行通知。很明显这种求职者和招聘公司之间的关系也是一种发布订阅模式。
为了更具体清晰明了,我们通过面向对象的方式进行实现。
定义招聘公司
// 定义发布者招聘公司
class CompanyObservable {
constructor() {
// 岗位
this.post = ''
// 订阅招聘信息的人的记录列表
this.subscribers = []
}
// 发布岗位
setPost(value) {
this.post = value
// 岗位更新了,通知所有的订阅者
this.notify()
}
// 添加订阅者
addDep(sub) {
// 把订阅者添加进记录列表
this.subscribers.push(sub)
}
// 发布信息
notify() {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers.forEach(sub => sub.update(this.post));
}
// 取消订阅
remove(sub) {
// 找到需要删除的订阅者
const index = this.subscribers.indexOf(sub)
// 删除订阅者
this.subscribers.splice(index, 1 )
}
}
// 实例化发布者招聘公司
const company = new CompanyObservable()
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
}
// 订阅
follow() {
company.addDep(this)
}
// 取消订阅
unfollow() {
company.remove(this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到的招聘信息:${content}`)
}
}
我们通过面向对象的方式实现了一遍求职者与招聘公司之间的代码逻辑关系,我们可以看到跟上面公众号的例子是一样的。接下来我们进行相关的测试。
我们通过实例化订阅者类 Subscriber 创建一个求职者------郭靖,然后进行订阅招聘公司的岗位信息,招聘公司发布岗位信息的测试。
// 创建订阅者郭靖
const guojing = new Subscriber('郭靖')
// 郭靖订阅招聘公司的岗位信息
guojing.follow()
// 发布者公司发布岗位
company.setPost('前端工程师')
上面测试代码最终会打印:郭靖收到的招聘信息:前端工程师。
上述例子只是一个订阅者,我们可以创建多个订阅者以体现文章开头对发布订阅者模式的定义:一对多的依赖关系。
// 创建订阅者杨过
const yangguo = new Subscriber('杨过')
// 创建订阅者欧阳锋
const ouyangfeng = new Subscriber('欧阳锋')
// 杨过订阅招聘公司的岗位信息
yangguo.follow()
// 欧阳锋订阅招聘公司的岗位信息
ouyangfeng.follow()
// 发布者招聘公司发布岗位信息
company.setPost('后端工程师')
上面的测试代码最终会打印:
郭靖收到的招聘信息:前端工程师
杨过收到的招聘信息:后端工程师
欧阳锋收到的招聘信息:后端工程师
招聘公司发现郭靖的信息作假,把他的信息删除了,这样郭靖将不再收到招聘公司新发布的信息。
// 郭靖信息作假,公司删除郭靖信息
company.remove(guojing)
// 公司再次发布招聘信息,郭靖不再收到招聘信息
company.setPost('运维工程师')
上面的测试代码最终打印:
杨过收到的招聘信息:运维工程师
欧阳锋收到的招聘信息:运维工程师
郭靖被删除了,所以不会打印郭靖的信息。
上面的实现还存在一个问题,如果郭靖只是一个前端工程师,那么他只希望接收前端工程师的岗位信息,但目前他能接收招聘公司所有发布的岗位信息,所以我们需要增加一个类型 key,让订阅者只订阅自己喜欢的内容。
重新改写后的公司代码如下:
// 定义一家公司
class CompanyObservable {
constructor() {
// 岗位信息
this.post = {
// 前端岗位信息
frontEnd: '',
// 后端的岗位信息
backEnd: '',
// 运维的岗位信息
DevOps: ''
}
// 订阅招聘信息的人的记录列表
this.subscribers = {}
}
// 发布岗位
setPost(event, value) {
this.post[event] = value
// 岗位更新了,通知所有的订阅者
this.notify(event)
}
// 添加订阅者
addDep(event, sub) {
// 如果还没有订阅过此类消息,则给该类消息创建一个记录列表
if (!this.subscribers[event]) {
this.subscribers[event] = []
}
// 把订阅者添加进记录列表
this.subscribers[event].push(sub)
}
// 发布信息
notify(event) {
// 发布信息时就是把记录列表中的订阅者全部通知一次
this.subscribers[event].forEach(sub => sub.update(this.post[event]));
}
// 取消订阅
remove(event, sub) {
// 找到需要删除的订阅者
const index = this.subscribers[event].indexOf(sub)
// 删除订阅者
this.subscribers[event].splice(index, 1 )
}
}
// 实例化发布者招聘公司
const company = new CompanyObservable()
class Subscriber {
constructor(name) {
// 求职者名称
this._name = name
}
// 订阅,添加订阅事件类型参数
follow(event) {
company.addDep(event, this)
}
// 取消订阅
unfollow(event) {
company.remove(event, this)
}
// 接受招聘公司信息更新方法
update(content) {
console.log(`${this._name}收到的招聘信息:${content}`)
}
}
上面测试代码最终会打印:
郭靖收到的招聘信息:前端工程师
杨过收到的招聘信息:后端工程师
欧阳锋收到的招聘信息:运维工程师
我们可以看到,修改后我们可以让订阅者只订阅自己喜欢的内容了。
小结
通过上面的例子我们可以看到通过多个订阅者的测试,我们已经验证了文章开头对发布订阅模式的定义:对象间一种一对多的依赖关系 。网上很多作者就把这种模式叫观察者模式 ,也就是一个被观察者,以及多个观察者。当被观察者发生改变的时候,所有的观察者都得到通知。从代码组织结构上看 CompanyObservable 类的实例对象是 被观察者,Subscriber 类的实例对象则是 观察者,CompanyObservable 和 Subscriber 之间是强耦合的。我们知道招聘公司是可以有多个的,而我们上述的代码组织方式,是没办法实现多个招聘公司进行发布信息的,也就是多个发布者和多个订阅者 进行联系。在现实生活中,招聘公司一般是通过招聘平台是发布招聘信息的,求职者则是通过招聘平台进行信息订阅,招聘公司一般是不会直接和求职者联系发送招聘信息的。
所以接下来我们要实现一个中介的角色,招聘平台,然后通过招聘平台把求职者和公司之间联系起来,而且可以有多个公司进行发布招聘信息和多个求职者订阅自己喜欢的信息,这个中介角色就是 事件总线 ,也叫 发布订阅中心。