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 相关文章推荐
无缝滚动改进版支持上下左右滚动(封装成函数)
Dec 04 Javascript
解决JS浮点数运算出现Bug的方法
Mar 12 Javascript
纯JavaScript实现HTML5 Canvas六种特效滤镜示例
Jun 28 Javascript
javascript连续赋值问题
Jul 08 Javascript
JQuery EasyUI Layout 在from布局自适应窗口大小的实现方法
May 28 Javascript
Ubuntu系统下Angularjs开发环境安装
Sep 01 Javascript
RequireJS简易绘图程序开发
Oct 28 Javascript
关于Javascript中document.cookie的使用
Mar 08 Javascript
JS伪继承prototype实现方法示例
Jun 20 Javascript
微信小程序地图(map)组件点击(tap)获取经纬度的方法
Jan 10 Javascript
vue拖拽组件 vuedraggable API options实现盒子之间相互拖拽排序
Jul 08 Javascript
json_decode 索引为数字时自动排序问题解决方法
Mar 28 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 或网站服务器日志,并正确配置 PHP 安装(win+linux)
2012/05/05 PHP
php文件包含的几种方式总结
2019/09/19 PHP
php实现推荐功能的简单实例
2019/09/29 PHP
js模仿hover的具体实现代码
2013/12/30 Javascript
利用js读取动态网站从服务器端返回的数据
2014/02/10 Javascript
了解Javascript的模块化开发
2015/03/02 Javascript
JavaScript图像延迟加载库Echo.js
2016/04/05 Javascript
js中遍历对象的属性和值的方法
2016/07/27 Javascript
js发送短信倒计时的简单实现方法
2016/09/08 Javascript
前端框架Vue.js构建大型应用浅析
2016/09/12 Javascript
webpack常用配置项配置文件介绍
2016/11/07 Javascript
详解使用vue-router进行页面切换时滚动条位置与滚动监听事件
2017/03/08 Javascript
通过命令行创建vue项目的方法
2017/07/20 Javascript
在Swiper内如何制作CSS3动画效果示例代码
2017/12/07 Javascript
Vue下拉框回显并默认选中随机问题
2018/09/06 Javascript
jQuery+ajax实现批量删除功能完整示例
2019/06/06 jQuery
Bootstrap实现模态框效果
2019/09/30 Javascript
uni-app微信小程序登录并使用vuex存储登录状态的思路详解
2019/11/04 Javascript
[01:03]PWL开团时刻DAY6——别打我
2020/11/05 DOTA
如何处理Python3.4 使用pymssql 乱码问题
2016/01/08 Python
你眼中的Python大牛 应该都有这份书单
2017/10/31 Python
Python多线程中阻塞(join)与锁(Lock)使用误区解析
2018/04/27 Python
Python进阶:生成器 懒人版本的迭代器详解
2019/06/29 Python
在tensorflow中设置保存checkpoint的最大数量实例
2020/01/21 Python
基于Python3读写INI配置文件过程解析
2020/07/23 Python
Python list和str互转的实现示例
2020/11/16 Python
如何创建一个Flask项目并进行简单配置
2020/11/18 Python
HTML5之SVG 2D入门6—视窗坐标系与用户坐标系及变换概述
2013/01/30 HTML / CSS
澳大利亚最早和最古老的巨型游戏专家:Yardgames
2020/02/20 全球购物
物流专业毕业生推荐信范文
2013/11/18 职场文书
社保代办委托书怎么写
2014/10/06 职场文书
2015年师德表现自我评价
2015/03/05 职场文书
维护民族团结心得体会2016
2016/01/15 职场文书
干货分享:推荐信写作技巧!
2019/06/21 职场文书
Nginx速查手册及常见问题
2022/04/07 Servers
nginx七层负载均衡配置详解
2022/07/15 Servers