1. 概念
观察者模式也称作监听模式,即观察与被观察的关系,比如你在烧开水时看它有没有开,你就是观察者,水就是被观察者。观察者模式是指对象之间一对多的依赖关系,每当那个特定对象改变状态时,所有依赖于它的对象都会得到通知并被自动更新。
观察者模式可以有任意多个观察者对象同时监听某一个对象。监听某个对象的叫观察者(Observer),被监听的对象叫被观察者(Subject) 。被观察者对象在内容或状态发生改变时,会通知所有观察者,使它们能自动更新自己的信息。
设计UML如图:
2. 实现
定义被观察者(Subject)
// 马尼拉游戏,被观察者
class Subject {
constructor (name) {
// 局
this.name = name
this.several = 0
this.observers = []
}
getSeveral () {
return this.several
}
setSeveral (several) {
this.several = several
this.notifyAllObservers()
}
attach (observer) {
this.observers.push(observer)
}
notifyAllObservers () {
this.observers.forEach(observer => {
observer.update()
})
}
}
定义观察者(Observer)
// 参与者,观察者
class Observer {
constructor (name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update () {
console.log(`${this.name} 参与 ${this.subject.name}, 场次: ${this.subject.getSeveral()}`)
}
}
假设胖虎,大树,老吴一起去玩马尼拉,老王摆好棋局(创建被观察者:Subject)
const 马尼拉 = new Subject('马尼拉')
胖虎加入了棋局
const 胖虎 = new Observer('胖虎', 马尼拉)
大树加入了棋局
const 大树 = new Observer('大树', 马尼拉)
老吴加入了棋局
const 老吴 = new Observer('老吴', 马尼拉)
游戏开始,第一轮
马尼拉.setSeveral(1)
// 胖虎 参与 马尼拉, 场次: 1
// 大树 参与 马尼拉, 场次: 1
// 老吴 参与 马尼拉, 场次: 1
第二轮
马尼拉.setSeveral(2)
// 胖虎 参与 马尼拉, 场次: 2
// 大树 参与 马尼拉, 场次: 2
// 老吴 参与 马尼拉, 场次: 2
第三轮
马尼拉.setSeveral(3)
// 胖虎 参与 马尼拉, 场次: 3
// 大树 参与 马尼拉, 场次: 3
// 老吴 参与 马尼拉, 场次: 3
讲解
以上虚拟了一场马尼拉游戏。每当游戏开始时胖虎、大树和老吴都得到了通知。
3. 应用
根据其侧重,观察者模式可分为:
- 推模型
- 拉模型
推模型:被观察者会向观察者推送主题的详细信息。无论观察者是否需要这些信息。这种模型在实现时,会把被观察者中的信息通过update的参数传递给观察者。
如某应用 App 的服务要在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,这里你就需要向所有的 App客户端推送完整的通知消息:“本服务将在凌晨1:00开始进行维护,1:00-2:00期间所有服务将会暂停,感谢您的理解和支持!” 不管用户想不想知道,也不管用户会不会在这段期间去访问,消息都需要被准确无误地通知到。这就是典型的推模型的应用。
拉模型:被观察者在通知观察者时,只传少量信息。如果观察者需要更多信息,由观察者主动到被观察者中获取,相当于观察者从被观察者对象中拉数据。这种模型在实现时,会把被观察者对象自身通过update方法传递给观察者。
如某应用 App 有新的版本推出,则需要发送一个版本升级的通知消息,而这个通知消息只会简单地列出版本号和下载地址,如果你需要升级你的 App 还需要调用下载接口去下载安装包完成升级。这其实也可以理解成是拉模型。
推模型和拉模型,更多的是语义和逻辑上的区别,在代码实现的过程中,完全可以写成同时支持两种模型的通用参数形式。
4. 优劣
任何事物和规则都有优缺点,咱们需要很清楚才能用好它们。
优点
- 实现解耦
观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体观察者列表,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。
- 支持广播
观察者模式支持广播通讯。被观察者会向所有的登记过的观察者发出通知。
缺点
- 时间成本
如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 循环调用
如果在被观察者之间有循环依赖的话,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式是要特别注意这一点。
- 异步投递
如果对观察者的通知是通过另外的线程进行异步投递的话,系统必须保证投递是以自恰的方式进行的。
- 事件追溯
虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制使观察者知道所观察的对象是怎么发生变化的。
5. 与发布订阅模式的区别
面试题:观察者模式,和发布订阅模式,有什么区别?
从表面上看
- 观察者模式里,只有两个角色 —— 观察者 + 被观察者
- 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— 经纪人Broker
往更深层次讲
- 观察者和被观察者,是松耦合的关系
- 发布者和订阅者,则完全不存在耦合
从使用层面上讲
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern),比如我们常用的消息中间件
6. 总结
小到代码设计,大到架构设计,如事件驱动模型,再或一些产品的设计思路,都使用了这种模式,如邮件订阅、RSS Feed、YouTube及抖音、新闻的关注更新等等。非常重要!