美文网首页
2、观察者模式

2、观察者模式

作者: 懂会悟 | 来源:发表于2023-04-24 08:21 被阅读0次

    1、概念

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

    观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

    生活中的观察者模式

    恐怖故事:周一刚上班,前端开发张三就被产品经理李四拉进了一个钉钉群——“员工管理系统需求第99次变更群”。这个群里不仅有张三,还有后端开发 A,测试同学 B。三位技术同学看到这简单直白的群名便立刻做好了接受变更的准备、打算撸起袖子开始干了。此时李四却说:“别急,这个需求有问题,我需要和业务方再确认一下,大家先各忙各的吧”。这种情况下三位技术同学不必立刻投入工作,但他们都已经做好了本周需要做一个新需求的准备,时刻等待着产品经理的号召。

    一天过去了,两天过去了。周三下午,李四终于和业务方确认了所有的需求细节,于是在“员工管理系统需求第99次变更群”里大吼一声:“需求文档来了!”,随后甩出了"需求文档.zip"文件,同时@所有人。三位技术同学听到熟悉的“有人@我”提示音,立刻点开群进行群消息和群文件查收,随后根据群消息和群文件提供的需求信息,投入到了各自的开发里。上述这个过程,就是一个典型的观察者模式。

    在上述的过程中,需求文档(目标对象)的发布者只有一个——产品经理李四。而需求信息的接受者却有多个——前端、后端、测试同学,这些同学的共性就是他们需要根据需求信息开展自己后续的工作、因此都非常关心这个需求信息,于是不得不时刻关注着这个群的群消息提醒,他们是实打实的订阅者,即观察者对象。
    现在我们再回过头来看一遍开头我们提到的略显抽象的定义:

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

    在我们上文这个钉钉群里,一个需求信息对象对应了多个观察者(技术同学),当需求信息对象的状态发生变化(从无到有)时,产品经理通知了群里的所有同学,以便这些同学接收信息进而开展工作:角色划分 --> 状态变化 --> 发布者通知到订阅者,这就是观察者模式的“套路”。

    在实践中理解定义

    结合我们上面的分析,现在大家知道,在观察者模式里,至少应该有两个关键角色是一定要出现的——发布者和订阅者。用面向对象的方式表达的话,那就是要有两个类。

    // 定义发布者类
    class Publisher {
      constructor() {
        this.observers = []
        console.log('Publisher created')
      }
      // 增加订阅者
      add(observer) {
        console.log('Publisher.add invoked')
        this.observers.push(observer)
      }
      // 移除订阅者
      remove(observer) {
        console.log('Publisher.remove invoked')
        this.observers.forEach((item, i) => {
          if (item === observer) {
            this.observers.splice(i, 1)
          }
        })
      }
      // 通知所有订阅者
      notify() {
        console.log('Publisher.notify invoked')
        this.observers.forEach((observer) => {
          observer.update(this)
        })
      }
    }
    
    // 定义订阅者类
    class Observer {
      constructor() {
          console.log('Observer created')
      }
    
      update() {
          console.log('Observer.update invoked')
      }
    }
    

    以上,我们就完成了最基本的发布者和订阅者类的设计和编写。在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化。仍然以开篇的例子为例,我们让开发者们来监听需求文档(prd)的变化:

    // 定义一个具体的需求文档(prd)发布类
    class PrdPublisher extends Publisher {
      constructor() {
        super();
        // 初始化需求文档
        this.prdState = null;
        // 李四还没有拉群,开发群目前为空
        this.observers = [];
        console.log("PrdPublisher created");
      }
    
      // 该方法用于获取当前的prdState
      getState() {
        console.log("PrdPublisher.getState invoked");
        return this.prdState;
      }
    
      // 该方法用于改变prdState的值
      setState(state) {
        console.log("PrdPublisher.setState invoked");
        // prd的值发生改变
        this.prdState = state;
        // 需求文档变更,立刻通知所有开发者
        this.notify();
      }
    }
    

    作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活:

    class DeveloperObserver extends Observer {
      constructor() {
        super();
        // 需求文档一开始还不存在,prd初始为空对象
        this.prdState = {};
        console.log("DeveloperObserver created");
      }
    
      // 重写一个具体的update方法
      update(publisher) {
        console.log("DeveloperObserver.update invoked");
        // 更新需求文档
        this.prdState = publisher.getState();
        // 调用工作函数
        this.work();
      }
    
      // work方法,一个专门搬砖的方法
      work() {
        // 获取需求文档
        const prd = this.prdState;
        // 开始基于需求文档提供的信息搬砖。。。
        console.log("996 begins...");
      }
    }
    

    下面,我们可以 new 一个 PrdPublisher 对象(产品经理),她可以通过调用 setState 方法来更新需求文档。需求文档每次更新,都会紧接着调用 notify 方法来通知所有开发者,这就实现了定义里所谓的:

    目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

    // 创建订阅者:前端开发张三
    const zhangsan = new DeveloperObserver()
    // 创建订阅者:服务端开发小A(sorry。。。起名字真的太难了)
    const A = new DeveloperObserver()
    // 创建订阅者:测试同学小B
    const B = new DeveloperObserver()
    // 李四出现了
    const lisi = new PrdPublisher()
    // 需求文档出现了
    const prd = {
        // 具体的需求内容
        ...
    }
    // 李四开始拉群
    lisi.add(zhangsan)
    lisi.add(A)
    lisi.add(B)
    // 李四发送了需求文档,并@了所有人
    lisi.setState(prd)
    

    以上,就是观察者模式在代码世界里的完整实现流程了。

    相信走到这一步,大家对观察者模式的核心思想、基本实现模式都有了不错的掌握。

    2、与发布-订阅模式的区别

    可以看出,发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

    观察者模式有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加到目标(Subject) 中进行管理;另一方面,目标在触发事件的时候,也无法将通知操作(notify) 委托给事件通道,因此只能亲自去通知所有的观察者。
    我们再举例来看一下这两个模式的代码实现:

    • 订阅-发布模式
    class PubSub {
      constructor() {
        this.subscribers = [];
      }
    
      subscribe(topic, callback) {
        let callbacks = this.subscribers[topic];
        if (!callbacks) {
          this.subscribers[topic] = [callback];
        } else {
          callbacks.push(callback);
        }
      }
    
      publish(topic, ...args) {
        let callbacks = this.subscribers[topic] || [];
        callbacks.forEach((callback) => callback(...args));
      }
    }
    
    // 创建事件调度中心,为订阅者和发布者提供调度服务
    let pubSub = new PubSub();
    // A订阅了SMS事件(A只关注SMS本身,而不关心谁发布这个事件)
    pubSub.subscribe("SMS", console.log);
    // B订阅了SMS事件
    pubSub.subscribe("SMS", console.log);
    // C发布了SMS事件(C只关注SMS本身,不关心谁订阅了这个事件)
    pubSub.publish("SMS", "I published `SMS` event");
    
    • 观察者模式
    class Subject {
      constructor() {
        this.observers = [];
      }
    
      add(observer) {
        this.observers.push(observer);
      }
    
      notify(...args) {
        this.observers.forEach((observer) => observer.update(...args));
      }
    }
    
    class Observer {
      update(...args) {
        console.log(...args);
      }
    }
    
    // 创建观察者ob1
    let ob1 = new Observer();
    // 创建观察者ob2
    let ob2 = new Observer();
    // 创建目标sub
    let sub = new Subject();
    // 目标sub添加观察者ob1 (目标和观察者建立了依赖关系)
    sub.add(ob1);
    // 目标sub添加观察者ob2
    sub.add(ob2);
    // 目标sub触发SMS事件(目标主动通知观察者)
    sub.notify("I fired `SMS` event");
    

    观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由 调度中心 进行处理,订阅者和发布者 互不干扰 。这样一方面实现了 解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在 调度中心 做一些处理,类似于 权限控制之类的。

    回到我们上文的例子里。李四把所有的开发者拉了一个群,直接把需求文档丢给每一位群成员,这种发布者直接触及到订阅者的操作,叫观察者模式。但如果李四没有拉群,而是把需求文档上传到了公司统一的需求平台上,需求平台感知到文件的变化、自动通知了每一位订阅了该文件的开发者,这种发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式

    相信大家也已经看出来了,观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者。

    既生瑜,何生亮?既然有了观察者模式,为什么还需要发布-订阅模式呢?

    大家思考一下:为什么要有观察者模式?观察者模式,解决的其实是模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。但观察者模式仅仅是减少了耦合,并没有完全地解决耦合问题——被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用,两者之间还是有着说不清、道不明的关系。

    而发布-订阅模式,则是快刀斩乱麻了——发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上。发布-订阅模式下,实现了完全地解耦。

    但这并不意味着,发布-订阅模式就比观察者模式“高级”。在实际开发中,我们的模块解耦诉求并非总是需要它们完全解耦。如果两个模块之间本身存在关联,且这种关联是稳定的、必要的,那么使用观察者模式就足够了。而在模块与模块之间独立性较强、且没有必要单纯为了数据通信而强行为两者制造依赖的情况下,我们往往会倾向于使用发布-订阅模式。

    相关文章

      网友评论

          本文标题:2、观察者模式

          本文链接:https://www.haomeiwen.com/subject/hipeddtx.html