观察者模式
所谓观察者模式,其实就是为了实现松耦合(loosely coupled)。
用《Head First设计模式》里的气象站为例子,每当气象测量数据有更新,changed()方法就会被调用,于是我们可以在changed()方法里面,更新气象仪器上的数据,比如温度、气压等等。
但是这样写有个问题,就是如果以后我们想在changed()方法被调用时,更新更多的信息,比如说湿度,那就要去修改changed()方法的代码,这就是紧耦合的坏处。
怎么解决呢?使用观察者模式,面向接口编程,实现松耦合。
观察者模式里面,changed()方法所在的实例对象,就是被观察者(Subject,或者叫Observable),它只需维护一套观察者(Observer)的集合,这些Observer实现相同的接口,Subject只需要知道,通知Observer时,需要调用哪个统一方法就好了:
观察者模式的实现
//观察者模式
//内部基于发布订阅,收集观察者,状态变化后通知
//被观察者
class Subject{
constructor(name){
this.name = name
this.state = '开心'
this.observers = []
}
attach(o){
this.observers.push(o)
}
setState(newState){
this.state = newState
this.observers.forEach(o=>o.update(this))
}
}
//观察者
class Observer{
constructor(name){
this.name = name
}
update(baby){
console.log(`当前${this.name}被通知,小宝宝状态${baby.state}`)
}
}
let baby = new Subject('宝宝')
let father = new Observer('爸爸')
let mother = new Observer('妈妈')
// 被观察者接受观察
baby.attach(father)
baby.attach(mother)
// 被观察者修改状态
baby.setState('不开心')
// 👇观察者被触发
// 当前爸爸被通知,小宝宝状态不开心
// 当前妈妈被通知,小宝宝状态不开心
发布订阅模式
比如小红最近在淘宝网上看上一双鞋子,但是这双鞋卖光了,于是小红订阅上货提醒,等有货的时候就会自动通知,与此同时,小明,小花等也喜欢这双鞋,也订阅上货提醒;等来货的时候就通过依次会通知他们;
在上面的故事中,
小红,小明等属于订阅者,订阅该商品;
卖家属于发布者,当鞋子到了的时候,淘宝会依次通知小明,小红等;
淘宝网属于第三者Broker(调度中心),将两个不相关的人物关联起来。
发布订阅模式的优点
-
支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
-
发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变
对于第一点,我们日常工作中也经常使用到,比如我们的ajax请求,请求有成功(success)和失败(error)的回调函数,我们可以订阅ajax的success和error事件。我们并不关心对象在异步运行的状态,我们只关心success的时候或者error的时候我们要做点我们自己的事情就可以了。
发布订阅模式的缺点
- 创建订阅者需要消耗一定的时间和内存。
- 虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。
发布订阅模式的实现
- 首先要想好谁是发布者(比如上面的卖家)。
- 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如上面的买家收藏了卖家的店铺,卖家通过收藏了该店铺的一个列表名单)。
- 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
实现1:
class Center {
constructor() {
this.obj = {}
}
on(name, fn) {//订阅
if (!Array.isArray(this.obj[name])) {
this.obj[name] = []
}
this.off(name,fn)//去重
this.obj[name].push(fn)
}
off(name, fn) {//取消订阅
let tmpObj = this.obj[name]
for (let i = 0; i < tmpObj.length; i++) {
if (tmpObj[i] == fn) {
tmpObj.splice(i, 1)
break
}
}
}
emit(parmas) {//发布
for (let name in this.obj) {
this.obj[name].forEach((item) => {
item(parmas)
})
}
}
}
let a = new Center()
// 注意要在外部传入订阅函数,否则对象地址不同,无法匹配
let fna = (parmas) => {
console.log('用户1可以买' + parmas)
}
let fnb = (parmas) => {
console.log('用户2可以买' + parmas)
}
a.on('like', fna)//订阅
a.on('like', fnb)//订阅
a.emit('新品')//触发
// 用户1可以买新品
// 用户2可以买新品
a.off('like', fnb)//取消
a.emit('新品')//触发
// 用户1可以买新品
a.on('like', fna)//二次订阅,触发一次
a.emit('新品')//触发
//用户1可以买新品
实现2
let fs = require('fs')
//第三者Broker
let event = {
_arr:[],
on(fn){
this._arr.push(fn)
},
emit(){
this._arr.forEach(fn=>fn())
}
}
//订阅
event.on(function(){
console.log(Object.keys(person))
if(Object.keys(person).length===3){
console.log(person)
}
})
let person = {}
fs.readFile('./name.txt','utf8',(err,data)=>{
console.log(data)
person.name = data
//发布
event.emit()
})
fs.readFile('./age.txt','utf8',(err,data)=>{
person.age = data
event.emit()
})
fs.readFile('./sex.txt','utf8',(err,data)=>{
person.sex = data
event.emit()
})
观察者模式VS发布订阅模式
大概很多人都和我一样,觉得发布订阅模式里的Publisher,就是观察者模式里的Subject,而Subscriber,就是Observer。Publisher变化时,就主动去通知Subscriber。
其实并不是。
在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。
互不相识?那他们之间如何交流?
答案是,通过第三者,也就是在消息队列里面,我们常说的经纪人Broker。
发布者只需告诉Broker,我要发的消息,topic是AAA;
订阅者只需告诉Broker,我要订阅topic是AAA的消息;
于是,当Broker收到发布者发过来消息,并且topic是AAA时,就会把消息推送给订阅了topic是AAA的订阅者。当然也有可能是订阅者自己过来拉取,看具体实现。
发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的。
放一张极简的图,给大家对比一下这两个模式的区别:
总结
- 从表面上看:
观察者模式里,只有两个角色 —— 观察者 + 被观察者
而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人Broker
- 往更深层次讲:
观察者和被观察者,是松耦合的关系
发布者和订阅者,则完全不存在耦合
- 从使用层面上讲:
观察者模式,多用于单个应用内部
发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件
参考文章:
https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c
https://www.cnblogs.com/tugenhua0707/p/4687947.html
https://zhuanlan.zhihu.com/p/51357583