前言
前两天掘金看Promise实现的时候,文章提到then的回调跟观察者模式有关。脑子里回想下观察者模式,在项目里明明用到很多次,但是具体又说不出来,干脆先去把观察者模式认真撸一遍再回来看,于是有了这篇文章。。。
观察者模式
观察者模式的定义:
观察者模式指的是一个对象(Subject)维持一系列依赖于它的对象(Observer),当有关状态发生变更时 Subject 对象则通知一系列 Observer 对象进行更新。
简单来说就是 A 对 B 的某个属性敏感,当B的这个属性发生变化时,A要及时做出相应的处理。
此时A就是观察者Observer,B就是被观察者Subject。
通过一个热水器的例子来实现以下观察者模式:
- 温度检测装置可以获取到水的实时温度,用来判断水是不是烧开了。
需求:在水加热到100℃时关闭电源停止加热。
一般的实现方式:
由于热水器控制台并不知道水是不是烧开了,为了完成任务。所以就导致控制台不停的问温测装置现在水烧开了吗?水烧开了吗?烧开了我好断电完成任务,温测装置这边烦都烦死了。
观察者模式实现:
温测装置把控制台怼一顿,然后告诉他水开了我会告诉你的,你丫等我通知你吧,别来烦我了。
需求分析:
- 热水器控制台需要有一个被通知后断电的方法。
- 温测装置需要提供注册观察者(也就是天天追着要温度数据的那些人)的方法、水沸腾时通知所有已注册观察者的方法,还有移除观察者的方法。
- 为了拥有更好的扩展性,所以被温测装置里要有一个数组用来存观察者。
所以核心的就是怎么实现属性发生变化时,及时通知所有已注册的观察者?
答案就是采用Setter实现,或者Object.defineproperty,下面上代码
新建观察者和主题的接口
// 观察者
interface Observer {
update(params: any);
}
// 被观察主题
interface Subject {
registerObserver(observer: Observer);
removeObserver(observer: Observer);
notifyObserver();
}
热水器控制台实现观察者接口
class WaterHeaterControl implements Observer {
update(arg: boolean) {
if (arg) {
console.log('主人水烧好了,已自动为您切断电源!');
}
}
}
温度检测装置实现主题发布抽象类
class WaterTemperature implements Subject {
/**观察者 */
private observers: Observer[] = [];
private _isBoiling: boolean;
/**是否沸腾 */
set isBoiling(v: boolean) {
this._isBoiling = v;
this.notifyObserver();
}
get isBoiling() {
return this._isBoiling;
}
/**
* 注册观察者
* @param observer 观察者
*/
registerObserver(observer: Observer): void {
if (!this.observers.some(item => { item === observer })) {
this.observers.push(observer);
}
}
/**移除观察者 */
removeObserver(observer: Observer) {
if (this.observers.length > 0) {
this.observers = this.observers.filter(item => item !== observer);
}
}
/**通知所有观察者 */
notifyObserver() {
this.observers.forEach(item => {
item.update(this.isBoiling);
})
}
}
最终结果:
const waterTemperature = new WaterTemperature();
const waterHeaterControl = new WaterHeaterControl();
waterTemperature.registerObserver(waterHeaterControl);
waterTemperature.isBoiling = true;
至此,需求完美实现了,并且以后有其他模块也需要知道水是否沸腾的时候,只需要把这个模块实现观察者模式的接口,并且注册一下就可以在水开时,及时得到通知。
发布订阅模式
发布订阅模式其实就是观察者模式的进阶版,他们实现的功能基本是一致的。
但发布订阅模式更侧重于事件,当订阅的某个事件被触发时,订阅此事件的所有对象将会得到通知。
需求:新闻社发布新闻后,希望所有订阅的人都会得到通知。读者需要在有新闻发布时,能马上看到。
需求分析:
- 读者群体并不清楚有哪些新闻社,新闻社也不知道具体哪些读者订阅了他们,所以需要有一个类似于中间方的人帮他们进行操作。
- 假设这个中间人是一个新闻app,那么核心就在于这个app提供的功能。
- 对于发布方也就是新闻社来说,app必须具有发布功能。
- 对于读者群体,app必须支持可订阅,可取消的功能。
class Events {
/**事件对象 */
eventObj = {};
constructor() { }
/**
* 发布
* @param topic 要发布的主题
* @param arg 作为事件发送的数据
*/
publish(topic: string, ...args: any[]) {
if (this.eventObj.hasOwnProperty(topic)) {
const fn = this.eventObj[topic];
fn.forEach(fn => {
fn.apply(this, args);
});
}
}
/**
* 订阅
* @param topic 要订阅的主题
* @param handlers 事件处理函数
*/
subscribe(topic: string, ...handlers: Function[]): void {
if (!this.eventObj.hasOwnProperty(topic)) {
this.eventObj[topic] = [];
}
this.eventObj[topic].push(...handlers);
}
/**
* 取消订阅
* @param topic 订阅的主题
* @param handler 要取消的事件处理函数
*/
unsubscribe(topic: string, handler?: Function): boolean {
if (!this.eventObj.hasOwnProperty(topic)) {
return false;
}
if (handler) {
// 如果处理函数存在,则从主题处理数组中删除它
this.eventObj[topic] = this.eventObj[topic].filter(x => x !== handler);
return true;
} else {
// 否则直接删除该主题
return delete this.eventObj[topic];
}
}
}
新闻社
const events = new Events();
/**新闻社 */
interface NewsOffice {
// 发布新闻
releaseNews();
}
class HuPuNewsOffice implements NewsOffice {
// 发布新闻
releaseNews() {
console.log('新闻发布了!');
events.publish('news-release', '今天预计有大到暴雨!');
}
}
读者
/**读者 */
interface Reader {
// 订阅新闻
subscribeNews();
// 取消订阅
unscribeNews();
}
class XiaoMing implements Reader {
subscribeNews() {
events.subscribe('news-release', this.readNews);
}
unscribeNews() {
events.unsubscribe('news-release', this.readNews);
}
private readNews(arg) {
console.log(`今天新闻是:${arg}`);
console.log('今天又下雨啊,出门要记得带伞!');
}
}
class LaoWang implements Reader {
// 订阅新闻
subscribeNews() {
events.subscribe('news-release', this.readNews);
}
// 取消订阅
unscribeNews() {
events.unsubscribe('news-release', this.readNews);
}
private readNews(arg) {
console.log(`今天新闻是:${arg}`);
console.log('今天下雨适合睡觉!');
}
}
实现这个需求
const xiaoMing = new XiaoMing();
xiaoMing.subscribeNews();
const laoWang = new LaoWang();
laoWang.subscribeNews();
laoWang.unscribeNews();
const huPu = new HuPuNewsOffice();
huPu.releaseNews();
观察者模式和发布订阅的区别
它俩最显著的区别就是观察者模式中观察者和被观察者是互相知道对方的存在的,而在发布订阅模式中是不知道的。
- 比如上面的例子热水器控制台和温度检测装置是明显知道对方是谁的,所以控制台为了完成工作会不停的问水是不是烧开了,温度检测装置也知道他贼烦的那个温度检测装置是谁。
- 而在新闻社和读者的例子中,也可以说新闻社和读者是相互知道对方存在的,但是严格来讲,新闻社并不知道到底有谁订阅了新闻,读者也不知道到底是哪家新闻社发布了新闻,而且读者也不关注到底是谁发布的,我只是想看新闻而已。
- 所以从这个角度来说,相比于观察者模式下,发布订阅模式也有解耦的好处。因为热水器控制台必须要去温测装置那里注册,而读者群体和新闻社并不直接依赖,他们通过抽象出来的app建立了联系。