JavaScript设计模式之观察者模式与发布订阅模式详解


Posted in Javascript onMay 07, 2020

本文实例讲述了JavaScript设计模式之观察者模式与发布订阅模式。分享给大家供大家参考,具体如下:

学习了一段时间设计模式,当学到观察者模式和发布订阅模式的时候遇到了很大的问题,这两个模式有点类似,有点傻傻分不清楚,博客起因如此,开始对观察者和发布订阅开始了Google之旅。对整个学习过程做一个简单的记录。

观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。在观察模式中共存在两个角色观察者(Observer)被观察者(Subject),然而观察者模式在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。

其实观察者模式是一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象之上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单的将自己从中分离。

在观察者模式中一共分为这么几个角色:

  1. Subject:维护一系列观察者,方便添加或删除观察者
  2. Observer:为那些在目标状态发生改变时需要获得通知的对象提供一个更新接口
  3. ConcreteSuject:状态发生改变时,想Observer发送通知,存储ConcreteObserver的状态
  4. ConcreteObserver:具体的观察者
举例

举一个生活中的例子,公司老板可以为下面的工作人员分配认为,如果老板作为被观察者而存在,那么下面所属的那些员工则就作为观察者而存在,为工作人员分配的任务来通知下面的工作人员应该去做哪些工作。

通过上面的例子可以对观察者模式有一个简单的认知,接下来结合下面的这张图来再次分析一下上面的例子。

如果Subject = 老板的话,那么Observer N = 工作人员,如果细心观察的话会发现下图中莫名到的多了一个notify(),那么上述例子中的工作就是notify()

JavaScript设计模式之观察者模式与发布订阅模式详解

既然各个关系已经屡清楚了,下面通过代码来实现一下上述的例子:

// 观察者队列
class ObserverList{
  constructor(){
    this.observerList = {};
  }
  Add(obj,type = "any"){
    if(!this.observerList[type]){
      this.observerList[type] = [];
    }
    this.observerList[type].push(obj);
  }
  Count(type = "any"){
    return this.observerList[type].length;
  }
  Get(index,type = "any"){
    let len = this.observerList[type].length;
    if(index > -1 && index < len){
      return this.observerList[type][index]
    }
  }
  IndexOf(obj,startIndex,type = "any"){
    let i = startIndex,
      pointer = -1;
    let len = this.observerList[type].length;
    while(i < len){
      if(this.observerList[type][i] === obj){
        pointer = i;
      }
      i++;
    }
    return pointer;
  }
  RemoveIndexAt(index,type = "any"){
    let len = this.observerList[type].length;
    if(index === 0){
      this.observerList[type].shift();
    }
    else if(index === len-1){
      this.observerList[type].pop();
    }
    else{
      this.observerList[type].splice(index,1);
    }
  }
}
// 老板
class Boos {
  constructor(){
    this.observers = new ObserverList();
  }
  AddObserverList(observer,type){
    this.observers.Add(observer,type);
  }
  RemoveObserver(oberver,type){
    let i = this.observers.IndexOf(oberver,0,type);
    (i != -1) && this.observers.RemoveIndexAt(i,type);
  }
  Notify(type){
    let oberverCont = this.observers.Count(type);
    for(let i=0;i<oberverCont;i++){
      let emp = this.observers.Get(i,type);
      emp && emp(type);
    }
  }
}
class Employees {
 constructor(name){
  this.name = name;
 }
 getName(){
  return this.name;
 }
}
class Work {
 married(name){
  console.log(`${name}上班`);
 }
 unemployment(name){
  console.log(`${name}出差`);
 }
 writing(name){
  console.log(`${name}写作`);
 }
 writeCode(name){
  console.log(`${name}打代码`);
 }
}
let MyBoos = new Boos();
let work = new Work();
let aaron = new Employees("Aaron");
let angie = new Employees("Angie");
let aaronName = aaron.getName();
let angieName = angie.getName();
MyBoos.AddObserverList(work.married,aaronName);
MyBoos.AddObserverList(work.writeCode,aaronName);
MyBoos.AddObserverList(work.writing,aaronName);
MyBoos.RemoveObserver(work.writing,aaronName);
MyBoos.Notify(aaronName);

MyBoos.AddObserverList(work.married,angieName);
MyBoos.AddObserverList(work.unemployment,angieName);
MyBoos.Notify(angieName);
// Aaron上班
// Aaron打代码
// Angie上班
// Angie出差

代码里面完全遵循了流程图,Boos类作为被观察者而存在,Staff作为观察者,通过Work两者做关联。

如果相信的阅读上述代码的话可以出,其实观察者的核心代码就是peopleList这个对象,这个对象里面存放了N多个Array数组,通过run方法触发观察者的notify队列。观察者模式主要解决的问题就是,一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。当我们在做程序设计的时候,当一个目标对象的状态发生改变,所有的观察者对象都将得到通知,进行广播通知的时候,就可以使用观察者模式啦。

优点
  1. 观察者和被观察者是抽象耦合的。
  2. 建立一套触发机制。
缺点
  1. 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  3. 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
小结

对于观察者模式在被观察者中有一个用于存储观察者对象的list队列,通过统一的方法触发,目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。

发布/订阅模式

在发布订阅模式上卡了很久,但是废了好长时间没有搞明白,也不知道自己的疑问在哪,于是就疯狂Google不断地翻阅找到自己的疑问,个人觉得如果想要搞明白发布订阅模式首先要搞明白谁是发布者,谁是订阅者。

发布订阅:在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。-- 维基百科

看了半天没整明白(✿◡‿◡),惭愧...于是,学习的路途不能止步,继续...

大概很多人都和我一样,觉得发布订阅模式里的Publisher,就是观察者模式里的Subject,而Subscriber,就是ObserverPublisher变化时,就主动去通知Subscriber。其实并不是。在发布订阅模式里,发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。互不相识?那他们之间如何交流?

答案是,通过第三者,也就是在消息队列里面,我们常说的经纪人Broker

发布者只需告诉Broker,我要发的消息,topicAAA,订阅者只需告诉Broker,我要订阅topicAAA的消息,于是,当Broker收到发布者发过来消息,并且topicAAA时,就会把消息推送给订阅了topicAAA的订阅者。当然也有可能是订阅者自己过来拉取,看具体实现。

也就是说,发布订阅模式里,发布者和订阅者,不是松耦合,而是完全解耦的。

JavaScript设计模式之观察者模式与发布订阅模式详解

通过上面的描述终于有了一些眉目,再举一个生活中的例子,就拿微信公众号来说,每次微信公众号推送消息并不是一下子推送给微信的所有用户,而是选择性的推送给那些已经订阅了该公众号的人。

老规矩吧,用代码实现一下:

class Utils {
 constructor(){
  this.observerList = {};
 }
 Add(obj,type = "any"){
  if(!this.observerList[type]){
   this.observerList[type] = [];
  }
  this.observerList[type].push(obj);
 }
 Count(type = "any"){
  return this.observerList[type].length;
 }
 Get(index,type = "any"){
  let len = this.observerList[type].length;
  if(index > -1 && index < len){
   return this.observerList[type][index]
  }
 }
 IndexOf(obj,startIndex,type = "any"){
  let i = startIndex,
    pointer = -1;
  let len = this.observerList[type].length;
  while(i < len){
   if(this.observerList[type][i] === obj){
    pointer = i;
   }
   i++;
  }
  return pointer;
 }
}
// 订阅者
class Subscribe extends Utils {};
// 发布者
class Publish extends Utils {};
// 中转站
class Broker {
 constructor(){
  this.publish = new Publish();
  this.subscribe = new Subscribe();
 }
 // 订阅
 Subscribe(fn,key){
  this.subscribe.Add(fn,key);
 }
 // 发布
 Release(fn,key){
  this.publish.Add(fn,key);
 }
 Run(key = "any"){
  let publishList = this.publish.observerList;
  let subscribeList = this.subscribe.observerList;
  if(!publishList[key] || !subscribeList[key]) throw "No subscribers or published messages";
  let pub = publishList[key];
  let sub = subscribeList[key];
  let arr = [...pub,...sub];
  while(arr.length){
   let item = arr.shift();
   item(key);
  }
 }
}
class Employees {
 constructor(name){
  this.name = name;
 }
 getName(){
  let {name} = this;
  return name;
 }
 receivedMessage(key,name){
  console.log(`${name}收到了${key}发来的消息`);
 }
}
class Public {
 constructor(name){
  this.name = name;
 }
 getName(){
  let {name} = this;
  return name;
 }
 sendMessage(key){
  console.log(`${key}发送了一条消息`);
 }
}
let broker = new Broker();
let SundayPublic = new Public("Sunday");
let MayPublic = new Public("May");
let Angie = new Employees("Angie");
let Aaron = new Employees("Aaron");
broker.Subscribe(() => {
 Angie.receivedMessage(SundayPublic.getName(),Angie.getName());
},SundayPublic.getName());
broker.Subscribe(() => {
 Angie.receivedMessage(SundayPublic.getName(),Aaron.getName());
},SundayPublic.getName());
broker.Subscribe(() => {
 Aaron.receivedMessage(MayPublic.getName(),Aaron.getName());
},MayPublic.getName());
broker.Release(MayPublic.sendMessage,MayPublic.getName());
broker.Release(SundayPublic.sendMessage,SundayPublic.getName());
broker.Run(SundayPublic.getName());
broker.Run(MayPublic.getName());
// Sunday发送了一条消息
// Angie收到了Sunday发来的消息
// Aaron收到了Sunday发来的消息
// May发送了一条消息
// Aaron收到了May发来的消息

通过上面的输出结果可以得出,只要订阅过该公众号的用户,只要公众号发送一条消息,所有订阅过该条消息的用户都是可以收到这条消息。虽然代码有点多,但是确确实实能够体现发布订阅模式的魅力,很不错。

优点
  1. 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
  2. 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;同理卖家(发布者)它只需要将鞋子来货的这件事告诉订阅者(买家),他不管买家到底买还是不买,还是买其他卖家的。只要鞋子到货了就通知订阅者即可。
缺点
  1. 创建订阅者需要消耗一定的时间和内存。
  2. 虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护。
小结

发布订阅模式可以降低发布者与订阅者之间的耦合程度,两者之间从来不关系你是谁,你要作什么?订阅者只需要跟随发布者,若发布者发生变化就会通知订阅者应该也做出相对于的变化。发布者与订阅者之间不存在直接通信,他们所有的一切事情都是通过中介者相互通信,它过滤所有传入的消息并相应地分发它们。发布订阅模式可用于在不同系统组件之间传递消息的模式,而这些组件不知道关于彼此身份的任何信息。

观察者模式与发布订阅的区别

  1. Observer模式中,Observers知道Subject,同时Subject还保留了Observers的记录。然而,在发布者/订阅者中,发布者和订阅者不需要彼此了解。他们只是在消息队列或代理的帮助下进行通信。
  2. Publisher / Subscriber模式中,组件是松散耦合的,而不是Observer模式。
  3. 观察者模式主要以同步方式实现,即当某些事件发生时,Subject调用其所有观察者的适当方法。发布者/订阅者在大多情况下是异步方式(使用消息队列)。
  4. 观察者模式需要在单个应用程序地址空间中实现。另一方面,发布者/订阅者模式更像是跨应用程序模式。

JavaScript设计模式之观察者模式与发布订阅模式详解

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的。如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。发布订阅模式相比观察者模式多了个事件通道,订阅者和发布者不是直接关联的。目标和观察者是直接联系在一起的。观察者把自身添加到了目标对象中,可见和发布订阅模式差别还是很大的。在这种模式下,目标更像一个发布者,他让添加进来的所有观察者都执行了传入的函数,而观察者就像一个订阅者。虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。

总结

虽然在学习这两种模式的时候有很多的坎坷,最终还是按照自己的理解写出来了两个案例。或许理解的有偏差,如果哪里有问题,希望大家在下面留言指正,我会尽快做出修复的。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
js document.write()使用介绍
Feb 21 Javascript
仿JQuery输写高效JSLite代码的一些技巧
Jan 13 Javascript
javascript通过元素id和name直接取得元素的方法
Apr 28 Javascript
使用AngularJS制作一个简单的RSS阅读器的教程
Jun 18 Javascript
javascript中数组方法汇总
Jul 07 Javascript
javascript弹出拖动窗口
Aug 11 Javascript
javascript跨域总结之window.name实现的跨域数据传输
Nov 01 Javascript
jQuery弹出div层过2秒自动消失
Nov 29 Javascript
通过sails和阿里大于实现短信验证
Jan 04 Javascript
基于vue+ bootstrap实现图片上传图片展示功能
May 17 Javascript
jQuery实现对网页节点的增删改查功能示例
Sep 18 jQuery
微信小程序实现上拉加载功能示例【加载更多数据/触底加载/点击加载更多数据】
May 29 Javascript
微信小程序pinker组件使用实现自动相减日期
May 07 #Javascript
简单了解JavaScript弹窗实现代码
May 07 #Javascript
angular组件间传值测试的方法详解
May 07 #Javascript
Node.js API详解之 timer模块用法实例分析
May 07 #Javascript
JS面试题中深拷贝的实现讲解
May 07 #Javascript
javascript 代码是如何被压缩的示例代码
May 06 #Javascript
Layui弹框中数据表格中可双击选择一条数据的实现
May 06 #Javascript
You might like
php分页思路以及在ZF中的使用
2012/05/30 PHP
php中利用str_pad函数生成数字递增形式的产品编号
2013/09/30 PHP
tp5递归 无限级分类详解
2019/10/18 PHP
JavaScript 面向对象与原型
2015/04/10 Javascript
Bootstrap框架下下拉框select搜索功能
2020/03/26 Javascript
简单实现轮播图效果的实例
2016/07/15 Javascript
JS实现的多张图片轮流播放幻灯片效果
2016/07/22 Javascript
jQuery Ajax请求后台数据并在前台接收
2016/12/10 Javascript
手动初始化Angular的模块与控制器
2016/12/26 Javascript
js模拟百度模糊搜索的实例
2017/08/04 Javascript
Vue使用NPM方式搭建项目
2018/10/25 Javascript
vue 基于element-ui 分页组件封装的实例代码
2018/12/10 Javascript
使用Vue开发自己的Chrome扩展程序过程详解
2019/06/21 Javascript
vue设置一开始进入的页面教程
2019/10/28 Javascript
微信小程序图片自适应实现解析
2020/01/21 Javascript
使用Typescript和ES模块发布Node模块的方法
2020/05/25 Javascript
详解React 条件渲染
2020/07/08 Javascript
python使用socket进行简单网络连接的方法
2015/04/29 Python
Python环境下搭建属于自己的pip源的教程
2016/05/05 Python
Python实现的堆排序算法原理与用法实例分析
2017/11/22 Python
利用Python暴力破解zip文件口令的方法详解
2017/12/21 Python
基于Python的Post请求数据爬取的方法详解
2019/06/14 Python
解决python 3 urllib 没有 urlencode 属性的问题
2019/08/22 Python
基于python的BP神经网络及异或实现过程解析
2019/09/30 Python
Python打印特殊符号及对应编码解析
2020/05/07 Python
兰蔻俄罗斯官方网站:Lancome俄罗斯
2019/12/09 全球购物
写自荐信要注意什么
2013/12/26 职场文书
优秀班组长事迹
2014/05/31 职场文书
暑期学习心得体会
2014/09/02 职场文书
2015年公司新年寄语
2014/12/08 职场文书
构建和谐校园倡议书
2015/01/19 职场文书
入党个人总结范文
2015/03/02 职场文书
生产车间管理制度
2015/08/04 职场文书
PHP 对接美团大众点评团购券(门票)的开发步骤
2021/04/03 PHP
HTML+JS实现在线朗读器
2022/02/15 Javascript
MySQL实现配置主从复制项目实践
2022/03/31 MySQL