浅谈前端设计模式3:【观察者模式】和【发布订阅模式】的关系和区别

127 阅读4分钟

一、观察者模式

1.1、观察者模式概念

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

1.2、传统的观察者模式

UML类图

image.png

观察者模式的主要角色如下

  1. 抽象主题(Subject):即:被观察者。也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  2. 抽象观察者(Observer):它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  3. 具体主题(Concrete Subject):即:具体被观察者。也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  4. 具体观察者(Concrete Observer):实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。

1.2、前端中的观察者模式

UML类图

image.png

1.4、观察者模式的优缺点

优点: 观察者与被观察者之间是属于轻度的关联关系,并且是抽象耦合的,这样,对于两者来说都比较容易进行扩展。

缺点: 观察者模式是一种常用的触发机制,它形成一条触发链,依次对各个观察者的方法进行处理。由于是链式触发,当观察者比较多的时候,性能问题是比较令人担忧的。并且,在链式结构中,比较容易出现循环引用的错误,造成系统假死。


二、【观察者模式】和【发布订阅模式】的关系和区别

  • 《Head First设计模式》里提到,发布者+订阅者 = 观察者模式。两者只是名字不太一样:发布者就是“主题”,订阅者就是“观察者”。
  • 《JavaScript设计模式》中,观察者模式又被称作发布-订阅者模式或消息机制,定义了一种依赖关系,解决了主题于观察者之间功能的耦合。

以上说法都不完全正确。这只是说明了两者有相似这处,但两者并不能画等号。

2.1、关系:发布订阅模式属于广义上的观察者模式

发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式。

2.2、区别:发布订阅模式多了事件通道(可以理解为是一个调度中心)。

在【观察者模式】中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。

 ╭─────────────╮  Fire Event  ╭──────────────╮
 │             │─────────────>│              │
 │   Subject   │              │   Observer   │
 │             │<─────────────│              │
 ╰─────────────╯  Subscribe   ╰──────────────╯

在【发布订阅模式】中,发布者和订阅者之间多了一个发布通道,一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件。

以此避免发布者和订阅者之间产生依赖关系,从而达到完全解耦。

 ╭─────────────╮                 ╭───────────────╮   Fire Event   ╭──────────────╮
 │             │  Publish Event  │               │───────────────>│              │
 │  Publisher  │────────────────>│ Event Channel │                │  Subscriber  │
 │             │                 │               │<───────────────│              │
 ╰─────────────╯                 ╰───────────────╯    Subscribe   ╰──────────────╯

从两张图片可以看到,最大的区别是调度的地方。

虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调度的。

从依赖关系上讲

  • 所以观察者模式中的观察者和被观察者,是松耦合的关系
  • 而发布/订阅模式中的- 发布者和订阅者,则完全不存在耦合。

从使用层面上讲

  • 观察者模式,多用于单个应用内部
  • 发布订阅模式,则更多的是一种跨应用的模式,比如我们常用的消息中间件

发布者直接触及到订阅者的操作,叫观察者模式。发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式

3、用前端代码具体实现【观察者模式】和【发布订阅模式】

一、观察者模式

  在对象之间定义一个一对多的依赖,当对象自身状态改变的时候,会自动通知给关心该状态的观察者。解决了主题对象与观察者之间功能的耦合,即一个对象的状态改变给其他对象通知的问题。

主题提供维护观察者的一系列方法,观察者提供更新接口。观察者把自己注册到主题里,在主题发生变化时候,调度观察者的更新方法。

// 主题(被观察者)
class Subject {
    constructor() {
        this.state = 0
        // 观察者列表
        this.observers = []
    }
    // 获取状态值
    getState() {
        return this.state
    }
    // 设置状态值
    setState(state) {
        this.state = state
        // 当自身状态改变时,通知所有观察者
        this.notify()
    }
    // 添加观察者
    add(observer) {
        this.observers.push(observer)
    }
    // 删除观察者
    remove(observer) {
        let index = this.observers.indexOf(observer)
        if (index > -1) {
            this.observers.splice(index, 1)
        }
    }
    //  通知所有观察者(调度观察者的更新方法)
    notify() {
        for (let observer of this.observers) {
            observer.update()
        }
    }
}

// 观察者类
class Observer {
    constructor(name) {
        this.name = name
    }
    // 观察目标更新时
    update() {
        console.log(`${this.name}观察到目标主题状态发生了改变`)
    }
}

// 主题(被观察者)
let subject = new Subject()

// 观察者
let obs1 = new Observer('观察者1')
let obs2 = new Observer('观察者2')

// 观察者把自己注册到主题里
subject.add(obs1)
subject.add(obs2)

// 主题发生变化时候,内部会通知观察者,调度观察者的更新方法。
subject.setState(1) 
// 输出内容如下:
// 观察者1观察到目标主题状态发生了改变
// 观察者2观察到目标主题状态发生了改变

二、发布订阅模式

  也是定义一对多的依赖关系,对象状态改变后,通知给所有关心这个状态的订阅者。发布订阅模式有订阅的动作,可以不和观察者直接产生联系,只要能订阅上关心的状态即可,通常可用第三媒介来做,而发布者也会利用第三媒介来通知订阅者。

// 发布订阅调度中心
class PubSub {
    constructor() {
        // 存储可以广播的主题
        this.topics = {}
    }
    // 订阅感兴趣的事件(带有特定的主题名称和回调函数),待执行当主题/事件时被观察到
    subscribe(topic, fn) {
        if (!this.topics[topic]) {
            this.topics[topic] = []
        }
        this.topics[topic].push(fn)
    }
    // 发布或广播感兴趣的事件(带有特定的主题名称和参数)
    publish(topic, ...arg) {
        for (let fn of this.topics[topic]) {
            fn.call(this, ...arg)
        }
    }
    // 退订指定主题/事件的某个回调函数或所有回调函数
    unSubscribe(topic, fn) {
        // 指定主题/事件的回调函数列表
        let fnList = this.topics[topic]       
        if (!fnList) {
            return false
        }
        // 如果没有指定回调函数,则退订所有,否则退订指定回调函数
        if (!fn) {
            fnList && (fnList.length = 0)
        } else {
            let index = fnList.indexOf(fn)
            fnList.splice(index, 1)
        }
    }
}

const pubSub = new PubSub()

// 订阅主题/事件(带有回调函数)
pubSub.subscribe('onwork', time => {
    console.log(`上班时间:${time}`)
})
pubSub.subscribe('offwork', time => {
    console.log(`下班时间:${time}`)
})
pubSub.subscribe('launch', time => {
    console.log(`午饭时间:${time}`)
})

// 发布主题/事件(带有参数),触发上面绑定的回调函数
pubSub.publish('onwork', '8:30')
pubSub.publish('launch', '12:00')
pubSub.publish('offwork', '17:30')

// 上班时间:8:30
// 午饭时间:12:00
// 下班时间:17:30

上面的代码就是发布订阅最重要的部分,虽然没有明显的实现观察者类和被观察者类,但观察者和被观察者在发布订阅模式中本来就不会有直接关联,都只需调用以上代码(事件总线)中的发布和订阅方法即可。

参考文献