设计模式 - 观察者模式 - 发布订阅模式

观察者模式和发布订阅模式是平常业务开发中最常见的设计模式,虽然网上大多数文章将二者归为一类,其实不然,它们两者之间还是有细微的差距。

先来个观察者模式的定义

  • 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的某个属性(或状态)发生变化时,会通知所有观察者对象,让它们自动更新

现实映射

  • 举一个🌰,高中的时候,我会经常去问老师问题,有时候遇到比较难的问题,老师一时半会解不开,老师会说**”你先去做其他的事情吧,一会儿我找到解题思路了来叫你”**。于是乎我先去做其他事情,等待老师的召唤。过了一会儿,老师叫另一个同学来叫我去办公室找他,于是我马上放下手中的活,冲向的老师的办公室……
  • 在这里例子里,我是一位观察者,而老师则是一位我观察的对象,当老师的状态发生了变化(指想出了题的思路),我就会接受到对应的信息,然后马上更新我自己的状态(指润去找老师)。

来点转换

  • 上述例子如果在发布-订阅模式里,我则摇身一变,变成了订阅者,专门订阅老师发布的通知信息,而老师则作为了发布者
  • 其实,上述例子还不能完全展示出定义所说的一对多关系,因为订阅者只有我一个人,但其实稍微扩展一下,变成多位同学向老师询问同一道题目,那这就是标准的观察者模式了,多位观察者“观察”老师的状态。

来点代码

  • 通过上述的定义和描述,大概可以知道,在观察者模式中,一共有两个类:发布者类和订阅者类。作为一个发布者,很容易可以想到它有下面几个基本方法:增加订阅者,通知订阅者,移除订阅者。思路有了,下面就直接实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 发布者类
class Publisher {
constructor() {
this.observers = [] // Observer -> 观察员
}

// 添加订阅者
add(observers) {
this.observers.push(...observers)
}
// 移除订阅者
remove(observer) {
this.observers.forEach((item, index) => {
if (item === observer) {
this.observer.splice(index, 1)
}
})
}
// 通知订阅者
notify() {
this.observers.forEach((item) => {
item.update(); // 注意,订阅者的方法应该它们本身定义的
})
}
}


  • 发布者基本类设计完毕,下面开始设计下订阅者,其实订阅者很简单,它最核心的就一个方法:收到发布者的信息后,去进行状态更新。如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
// 定义订阅者类
class Observer {
constructor() {
console.log('创建订阅者')
}

update() {
console.log('更新状态')
}
}


  • 好了,上述两段代码就是最基本的观察者模式实现,实际场景的观察者模式都是基于上面的代码进行迭代。比如下面我将会把之前说的实例进行代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 教师类
class Teachere extends Publisher {
constructor() {
super()
//
this.answers = null
this.observers = []
}

getAnswers() {
return this.answers
}

setAnswers(ques) {
this.answers = answers;
console.log('老师想出了答案,快来办公室找老师')
this.notify()
}

notify() {
const that = this;
this.observers.forEach(item => {
item.update(that)
})
}
}

// 学生类
class Student extends Observer {
constructor() {
console.log("我是学生")
}

update(info) {
const answer = info.getAnswers()
// ... 进行相关的更新操作
}
}

观察者模式和发布-订阅模式的区别

  • 其实在刚才的例子不算严格意义上的发布-订阅模式,因为发布-订阅模式中,发布者和订阅者二者之间是透明的,即它们是彼此感受不到对方的,无论是发布事件,还是订阅事件,都是交给一个统一的管理系统来进行处理的,这就类比于游戏原神中的每日委托一样,委托者在冒险家协会进行每日任务的委托,而旅行者-我则会去冒险家协会去领取每日任务,这个过程中,我们彼此感受不到对方,而是全权交给冒险家协会来进行管理的。
  • 如果放到Javascript世界中,无论是Vue的EventBus还是Nodejs中的EventEmitter类,它们的设计理念就是发布-订阅模式,即发布事件 - 订阅事件这一个过程。
1
2
3
4
5
6
7
8
// 订阅事件
event.on("callTeacher", function(...args){

})

// 发布事件
event.emit('callTeacher', arg1, arg2, arg3, ...args)

  • 很明显,和观察者模式不同的是,发布-订阅模式则将订阅者和发布者完全解耦,二者再也没有直接关联,它们的一切处理都统一交给第三方处理
  • 下面是一个简单的EventEmitter实现,为了加深对发布-订阅模式的认识。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 发布-订阅模式的管理站
class EventEmitter {
constructor() {
// handlers -> map,存储事件和回调之间的关系
this.handlers = {}
}

// eventname -> 事件名称 , cb -> 响应的回调函数
on(eventName, cb) {
if (!this.handlers[eventName]) {
this.handlers[eventName] = []
}

this.handlers[eventName].push(cb);
}

emit(eventName, ...args) {
if (this.handlers[eventName]) {
// 这里用拷贝,否则once的删除会导致handlers的forEach遍历失序
const handlers = this.handlers[eventName].slice()
handlers[eventName].forEach(cb => {
cb(...args)
})
}
}

off(eventName, cb) {
if (this.handlers[eventName]) {
this.handlers[eventName].forEach((item, index) => {
if (item === cb) {
this.handlers[eventName].splice(index, 1)
}
})
}
}

// 为事件注册单次监听器
once(eventName, cb) {
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}

请我喝杯咖啡吧~

支付宝
微信