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

发布时间 2023-09-01 18:58:45作者: beckyye

设计模式

wiki中将设计模式分为四类,分别是:

  • 创建模式(creational patterns)
  • 结构模式(structural patterns)
  • 行为模式(behavioral patterns)
  • 并发模式(concurrency patterns)

观察者/发布订阅模式属于其中的行为模式。

实际情境

公众号订阅

说到发布订阅,拿微信公众号举例,就很好理解,有n个人订阅了某公众号,在该公众号发布了推文后,这n个人就会收到推文发布的消息。

状态更新

在实际编程过程中,有一种情况很多人应该碰到过,就是在页面上完成一些与接口的交互操作后,要更新页面的某些状态,但这个状态由于某些原因无法立即获得,如果要得到状态的更新结果,可以有两种选择,一种是重复请求接口以获取,可以是用户手动刷新页面或请求接口、或者交互结束后自动开启接口轮询;第二种是建立长连接,等待服务端推送结果。第二种方式,就可以算作是一种发布-订阅,订阅者是客户端,订阅内容是页面状态,发布者就是服务端。

为什么说观察者/发布订阅属于行为模式从上述例子就可以理解,原本的行为是客户端主动去获取内容,应用了此模式后,行为变成了客户端订阅(发起长连接请求)、由服务端来推送消息,行为方式发生了变化;并且我们也可以看出,这样做提高了应用的性能,客户端不必要再多次地去发起请求,减少了建立网络请求的消耗。

定义

wiki中对这两个模式的作用有着相同的描述:

Define a one-to-many dependency between objects where a state change in one object results in all its dependents being notified and updated automatically.

翻译一下:

定义了对象之间一对多的依赖关系,其中一个对象的状态变更,会使其所有依赖对象收到通知并自动更新。

可以看出,这两个模式处理的是存在一对多依赖关系的双方之间的交互行为。

虽然描述相同,但两者的实际实现还是存在区别,我们可以再进一步了解。

观察者模式

在这种模式下,一个名为主体的对象会维护其依赖对象(即观察者)的列表,并自动通知它们任何状态变化,通常是通过调用它们的方法。

该模式通常用于在事件驱动软件中实现分布式事件处理系统。在这类系统中,主体通常被称为“事件流”或“事件流源”,而观察者则被称为“事件汇”。

大多数现代编程语言都包含实现观察者模式组件的内置事件结构。虽然不是强制性的,大多数观察者实现都使用后台线程监听主体事件和内核提供的其他支持机制。

发布-订阅模式

在软件架构中,“发布-订阅”是一种消息传递模式,在这种模式下,发布者将消息分类,再由订阅者接收。这与典型的由发布者直接将消息传递给订阅者的消息传递模式形成鲜明对比。

同样,订阅者对一个或多个类别表示感兴趣,只是接收感兴趣的信息,但并不知道有哪些发布者(如果有的话)。

发布-订阅是消息队列模式的同类,通常是更大的面向消息的中间件系统的一部分。大多数消息传递系统的API中同时支持发布/订阅和消息队列模型;比如Java消息服务(JMS)。

该模式提供了更高的网络可扩展性和更动态的网络拓扑结构,但也因此降低了修改发布者和发布数据结构的灵活性。

由上述两个定义可知,在发布-订阅模式中,发布者和订阅者的耦合度更低,订阅者并不知道消息的发布者,在这种模式下,通常会存在一个第三方的订阅中心,订阅中心接收到发布者的消息,然后再将消息分发给订阅者;而在观察者模式中,观察者是通过在主体身上放置监听器从而直接观察主体,相当于是发布者(主体)直接将消息传递给订阅者(观察者),此时两者的耦合性更高,这种模式下,观察者需要实现统一的接口以供主体调用,而主体则需要维护一个观察者的集合。

解决的问题

观察者模式可以解决以下问题:

  • 应在对象之间定义一对多的依赖关系时,不使对象紧密耦合
  • 当一个对象更改状态时,应自动更新不限数量的依赖对象
  • 一个对象可以通知多个其他对象

通过定义一个直接更新依赖对象状态的对象(主体)来定义对象间的一对多依赖关系是不灵活的,因为它将主体与特定的依赖对象联系在一起。然而,从性能角度,或者对象的实现是紧密耦合的(比如每秒执行数千次的低级内核结构),这可能是适用的。在某些情况下,紧耦合对象可能难以实现,而且不容易复用,因为它们会引用并感知许多具有不同接口的对象。

做法描述

那么软件设计中的观察者模式具体是怎么做的呢?以下是wiki给出的描述:

  • Define Subject and Observer objects.
  • When a subject changes state, all registered observers are notified and updated automatically (and probably asynchronously).

The sole responsibility of a subject is to maintain a list of observers and to notify them of state changes by calling their update() operation. The responsibility of observers is to register and unregister themselves with a subject (in order to be notified of state changes) and to update their state (to synchronize their state with the subject's state) when they are notified. This makes subject and observers loosely coupled. Subject and observers have no explicit knowledge of each other. Observers can be added and removed independently at run time. This notification-registration interaction is also known as publish-subscribe.

翻译一下:

  • 定义主体观察者对象
  • 当主体改变状态,所有注册的观察者都会收到通知并自动更新(可能是异步的)。

主体的唯一职责是维护一个观察者列表,并通过调用观察者的update()操作来通知它们状态的变更。观察者的职责是向主体注册或取消注册(以便收到状态变更的通知),并在收到通知时更新自己的状态(使自己的状态与主体的状态同步)。这使得主体和观察者松散耦合。主体和观察者彼此互不了解。观察者可以在运行时被单独添加和移除。这种‘通知-注册’交互也被称为‘发布-订阅’。

两者对比

根据wiki给出的观察者模式对应的做法描述,可以看到观察者模式中的'通知-注册'也被称为‘发布-订阅’,但开发者需要明白,消息传递模式中的‘发布-订阅’,发布者和订阅者没有直接接触,而是增加了一个消息分发中心,这个消息分发中心可以类比为代理模式中的代理,通常需要维护消息队列。

这种‘发布-订阅’模式的实现相比于普通的观察者模式具有以下两个优势:

  1. 松耦合

    发布者和订阅者不需要知道对方的相关信息

  2. 可扩展性

    如果有需要,可以随时增加新的订阅者来订阅发布者的消息,而发布者不需要做修改,甚至不需要知道

应用场景

发布-订阅模式从字面上来看,主要的应用场景就在于消息(如状态)的传递。

事件监听

根据上述定义部分的内容,可以看出事件处理就是应用了观察者模式,比如说给按钮添加事件处理程序:

<button id="addButton">
  新增待办事项
</button>
let button = document.querySeletor('#addButton');
button.addEventListener('click', () => {
  console.log('待办事项+1');
});

对于上述代码我们可以这么理解,id为addButton的按钮,通过addEventListener在自己身上按了一个观察者,这个观察者有一个函数可用处事件处理,在按钮的交互状态发生改变时(从普通状态变为活动状态),就通知这个观察者更新状态(即调用这个事件处理程序)。

另外,用过vue源码的小伙伴应该都知道,vue中的响应式就应用了观察者模式,每个组件data中的数据可以看作是主体,然后这些数据的观察者可以是vue实例对象、computed属性、watcher等等,这些观察者会被维护在data中每个数据对应的deps数组中。

事件总线

事件总线可以看作是一个订阅中心。比如在Vue中我们可以使用EventBus(本质上也是Vue实例)来作为事件中心,以完成事件的调度分发。使用方法如下:

// 创建一个事件总线并导出
const EventBus = new Vue();
export default EventBus;

// 在主文件中引入EventBus,并挂载到全局
import bus from 'EventBus文件路径';
Vue.prototype.bus = bus;

// 订阅事件“someEvent”
// 这里func指someEvent这个事件的监听函数,也即在收到消息后通知订阅者的方法
this.bus.$on('someEvent', func);

// 发布(触发)事件“someEvent”
// 这里params指someEvent这个事件被触发时回调函数接收的入参,也就是传递给订阅者的状态
this.bus.$emit('someEvent', params);

实现

在JavaScript我们可以通过以下代码来实现一个观察者模式:

let Subject = {
    _state: 0,
    _observers: [],
    add: function(observer) {
        this._observers.push(observer);
    },
    getState: function() {
        return this._state;
    },
    setState: function(value) {
        this._state = value;
        for (let i = 0; i < this._observers.length; i++)
        {
            this._observers[i].signal(this);
        }
    }
};

let Observer = {
    signal: function(subject) {
        let currentValue = subject.getState();
        console.log(currentValue);
    }
}

Subject.add(Observer); // 主体管理自己的依赖对象
Subject.setState(10);
//Output in console.log - 10

发布-订阅模式也可以通过一个简单的EventEmitter来实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }
  on(type, listern) {
    if (this.events[type]) {
      this.events[type].push(listener);
    } else {
      this.events[type] = [listener];
    }
  }
  emit(type, ...args) {
    if (this.events[type]) {
      this.events[type].forEach(fn => fn.call(this, ...args));
    }
  }
  once(type, listener) {
    const _ = this;
    function oneTime(...args) {
      listener.call(this, ...args);
      _.off(type, oneTime);
    }
    _.on(type, oneTime);
  }
  off(type, listener) {
    if (this.events[type]) {
      const index = this.events[type].indexOf(listener);
      this.events[type].splice(index, 1);
    }
  }
}

此处EventEmitter就是一个订阅中心。