美文网首页
设计模式系列二

设计模式系列二

作者: fanstastic | 来源:发表于2019-11-28 16:45 被阅读0次

发布订阅模式又叫观察者模式,它定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会被通知。在js中,我们一般用事件模型来替代传统的发布订阅模式

  1. 我们可以订阅ajax的error,succ事件。或者在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧之后发布这个事件。
  2. 发布订阅模式可以取代对象之间硬编码的通知机制,一个对象不再显式的调用另一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要做任何修改,同样,发布者需要改变时,也不会影响之前的订阅者。
  • dom事件
    比如我们订阅body上的click事件,当body被点击时,body节点便像订阅者发布这个消息。

  • 自定义事件
    如何实现发布订阅模式

  1. 首先指定谁是发布者
  2. 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
  3. 最后发布消息时,发布者会遍历缓存列表,依次触发里面存放的订阅者回调函数
let sales = {} // 定义一个售楼处作为发布者
sales.list = [] // 给这个发布者一个缓存队列存放订阅者的回调函数
sales.listen = (fn) => { this.list.push(fn) } 增加订阅者
sales.trigger = (...args) => { for(){ fn.apply(this, args) } } // 执行发布函数

至此我们实现了一个最简单的发布订阅模式,但是还有问题,我们接收到了发布者发布的每一个消息,如果我们只想接收自己感兴趣的消息怎么办。

let salesOffices = {} 
salesOffices.list = {}
salesOffices.listen = (key, fn) => {
  if (!this.list[key]) {
    this.list[key] = []
  }
  this.list[key].push(fn)
}
salesOffices.trigger = (...args) => {
  const key = args.shift()
  if (!salesOffices.list[key] || !salesOffices.list[key].length) {
    return false
  }
  for (let i=0, fn;fn = fn[i++]) {
      fn.apply(this, ...args)
  }
}
  • 发布订阅模式的通用实现
    如何让所有对象都拥有发布订阅功能
let event = {
  clientList: {},
  listen: (key, fn) => {
    if (!this.clientList[key]) {
        this.clientList[key] = []
    }
    this.clientList.push(fn)
  },
  trigger: (...args) => {
    let key = this.args.shift()
    if (!this.clientList[key] || !this.clientList[key].length) { return false } 
    for (let i=0, fn;fn = this.clientList[key][i++])
  }
}
const installEvent = (obj) => {
  for (let name in event) { obj[name] = event[name] } 
}
  • 取消订阅的事件
event.remove = (key, fn) => {
  const fns = this.clientList[key]
  if (!fns) { return false } else if (!fn) {
    fns && fns.length = 0;
  } else {
    for (let i=fns.length-1;i>=0;i--) {
      if (fns[i] == fn) fns.splice(i, 1)
    }
  }
}
  • 网站登录
    我们现在有多个模块,它们渲染的前提有一个共同的条件,必须先异步请求获取用户的登录信息。当用户登录成功时,登录模块只需要发布登录成功的消息,登录模块并不关心业务方需要做什么
$.ajax('http://xxx.com', function(data) {
  login.trigger('loginSucc', data) // 执行的时候触发所有的监听函数
})
let header = (() => {
  login.listen('loginSucc', (data) => { header.set(data) })
  return {
    set: (data) => {}
  }
})()
let nav = (() => {
  login.listen('loginSucc', (data) => nav.set(data))
  return { set: (data) => {} }
})()
  • 全局的发布-订阅对象
    我们可以使用一个全局的event对象来实现,订阅者不需要了解消息来自哪个发布者。
  • 模块间的通信
  • 必须先订阅再发布吗
    我们所了解的都是订阅者必须先订阅消息,随后才能接收发布者发布的消息。在某些情况下,我们需要先将发布者发布的信息保存下来,等有对象来订阅的时候,再把消息发布给订阅者。
  var event = (() => {
  _listen = (key, fn, cache) => {
     if (!cache[key]) { cache[key] = [] }
      cache[key].push(fn)
  }
  _remove = (key, cache, fn) => {
      if (cache[key] ){
          if (fn) {
              for (var i=cache[key].length;i>=0;i--) {
                if (cache[key][i]==fn) cache[key].splice(i, 1)  
              }
          } else { cache[key].length = 0 }
      } 
    }
  _trigger = () => {}
 _create = (namespace) => {
    
  }
  return {
    
  }
})()
  • 命令模式
    借助命令对象(command)的帮助,解开按钮和负责具体行为对象之间的耦合,用户并不知道command对象具体是一个什么对象。
    命令对象让需要执行的对象和需要执行的方法隐藏,命令模式的接收者被当成命令对象的属性

  • 撤销一系列指令

let Ryu = {
  attack: () => {},
  defense: () => {},
  jump: () => {},
  crouch: () => {}
}
  • 宏命令
let MacroCommand = function() {
  return {
    commandList: [],
    add: function(command) {
        this.commandList.push(command)
    },
    excute: function() {
        for (var i=0, command; command=this.commandList[i++]){
            command.excute()
        }  
    }
  }
}
  • 抽象类在组合模式中的作用
    组合模式最大的优点就是可以一致的对待组合对象和基本对象。客户不需要知道当前处理的是宏命令还是普通命令,只要它是一个命令,并且有execute方法,就可以被添加到树中。
    在java中,实现组合模式的关键是component类和leaf类都必须继承自component抽象类。这个component抽象类既代表组合对象,又代表叶对象。它能够保证组合对象和叶对象拥有同样的名字的方法,从而可以对同一消息做出反馈。组合对象和叶子对象的具体类型被隐藏在component抽象类后面。
    针对compenent类来编写程序,客户操作的始终是Compenent对象,而不用区分到底是组合对象还是叶对象。所以我们往同一个对象里的add方法里,既可以添加组合对象,也可以添加叶对象。
public abstract class Component {
  public void add (Component child) {}
  public void remove(Component child) {}
}
public class Composite extends Component {
  public void add (Component child) {}
  public void remove (Component child) {}
}
public class Leaf extends Component {
  public void 
}

js的对象天生具有多态性

  • 组合模式的例子-扫描文件夹
    文件夹和文件之间的关系。
var Folder = function(name) {
  this.name = name
  this.files = []
}
Folder.prototype.add = function (file) {
   this.files.push(file)
}
Folder.prototype.scan = () => {
  conso;le.log(sao miao)
  for (let i = 0, file; file = this.files[i++]) { file.scan() }
}
  1. 组合模式不是父子关系
  2. 对叶操作的一致性
    组合模式除了要求组合对象和叶对象拥有相同的接口之外,就是ui一组叶对象的操作必须有一致性
  • 引用父对象
    组合对象保存了子节点的引用,此时树结构是从上至下的。
let Folder = function(name) {
  this.name = name
  this.parent = null
  this.files=[]
}
Folder.prototype.add = (file) => {
  file.parent = this
  this.files.push(file)
}

Folder.prototype.scan = () => {
  for (let i=0, file;file = this.files[i++]) {
    file.scan()
  }
}

Folder.prototype.remove = () => {
  if (!this.parent) { return }
  for (let files = this.parent.files, l=files.length-1;l>=0;l--) {
    if (files[l] == this) { files.splice(l,1) }
  }
}
  • 模板方法模式
    模板方法是一种基于继承的设计模式, 模板方法由两部分构成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构。
    假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。在模板方法中,子类实现的相同部分被上移到父类中,而将不同的部分留待子类实现。
let Beverage = function() {}
Beverage.prototype.boilWater = function() {}
Beverage.prototype.brew = function(){}
Beverage.prototype.init = function() {
  this.boilWater()
  this.brew()
}
  • 创建Coffee子类和Tea子类
    饮料在这里是一个抽象的存在。接下来我们要创建咖啡类和茶类,并让它们继承饮料类。
let Coffee = function() {}
Coffee.prototype = new Beverage()
Coffee.prototype.brew = function() {}
Coffee.prototype.pourInCup = function() {}
Coffee.prototype.addCondiments = function() {}
let coffee = new Coffee()
coffee.init()

当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型都没有init方法,所以该请求会顺着原型链,被委托给Beverage原型上的init方法。
而init方法中已经规定好了泡饮料的顺序。
所以其中Beverage.prototype.init被称为模板方法,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

  • 抽象类的作用
    在java中,类分为两种,一种具体类,一种是抽象类。具体类可以被实例化,抽象类不能被实例化。如果有人写了抽象类,那么这些抽象类一定是用来被某些具体类继承的。
    把对象的真正类型隐藏在抽象类或者接口之后,这些对象才可以被互相替换使用。除了用于向上转型,抽象类也可以表示契约。继承抽象类的所有子类都都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为了给子类定义公共接口。如果在子类中删掉了这些方法中的某一个,那么将不能通过编译器的检查。
    比如如果在coffee子类中没有实现对应的brew方法,那么久无法得到咖啡。既然父类规定了子类的方法和顺序,子类基于应该拥有这些方法,并提供正确的实现。
    js中没有抽象类,抽象类的一个作用是隐藏对象的具体类型。js不需要隐藏对象的类型。
    当我们在js中使用原型继承模拟类式继承时,并没有编译器帮助我们进行类型的检查,我们也没办法保证子类会重写父类。

  • 模板方法模式的使用场景

  • 钩子方法
    利用挂载到原型上的钩子方法判断公共方法是否被执行,钩子方法默认返回boolean值。

  • 享元模式
    使用享元模式的关键是区分外部状态和内部状态,是否能够被实例共享是划分的关键。可以被对象共享的属性划分为内部状态,而外部状态取决于具体的场景,并根据场景而变化。

  • 享元模式的通用结构
    上面的例子中,我们还有两个问题,一是通过构造函数new出了两个model对象,在其他系统中,也许并不是一开始就需要所有对象。二是给model对象手动设置了外部状态,在复杂系统中,外部状态可能相当复杂。
    我们通过对象工厂解决第一个问题,只有某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器记录对象的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

  • 解决上传时的对象爆炸问题
    假设有flash和插件两种上传方式,当用户上传时,都会调用window下的全局对象的js函数。

let id=0;
window.startUpload = (uploadType, files) => {
  for (let i=0,file;file=files[i++];) {
    let uploadObj = new Upload(uploadType, file.fileName, file.fileSize)
    uploadObj.init(id++)
  }
}
let Upload = function(uploadType, fileName, fileSize) {
   this.uploadType = uploadType
    this.fileName = fileName
    this.fileSize = fileSize
    this.dom = null
}
Upload.prototype.init = (id) => {
  let that = this
  this.id = id
  this.dom = document.createElement('div')
  this.dom.innerHTML = '...'
  this.dom.querySelector('.delFile').onClick = () => {
    that.delFile()
  }
  document.body.appendChild(this.dom)
}
Upload.prototype.delFile = () => {
  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom)
  }
 
  if (window.confirm('xxx')) { return this.dom.parentNode.removeChild(this.dom) }
}
  • 享元模式重构文件上传
    首先确定插件类型uploadType是内部状态,而fileName和fileSize是根据场景变化,每个文件fileName和fileSize都不一样,它们没办法被共享,它们只能被划分为外部状态。
  1. 剥离外部状态
let Upload = function(uploadType) {
  this.uploadType = uploadType
}
Upload.prototype.delFile = function(id) {
  UploadManager.setExternalState(id, this) // 把当前id对应的对象的fileSize值设置到共享对象中
  ...
}

在开始读取文件前,需要读取文件的实际大小,而文件的实际大小被存储在
setExternalState当中,所以需要通过setExternalState方法给共享对象设置正确的fileSize

接下来定义一个工厂来创建upload对象,如果某种内部状态对应的共享对象被创建过,那么直接返回这个对象,否则创建一个新对象:

let UploadFac = (function(){
  let obj = {}
  return {
    create: function(uploadType) {
        if (obj[uploadType]) { return  obj[uploadType]}
        return obj[uploadType] = new Upload(uploadType)
    }
  }
})()
  • 管理器封装外部状态
    管理器负责向UploadFac提交创建对象的请求,并用一个uploaddatabase保存所有upload对象的外部状态,以便在程序运行中给upload共享对象设置外部状态。
let uploadmanage = (() => {
  let uploaddatabase = {}
  return {
    add: (id, uploadType, fileName, fileSize) => {
       let weightObj = UploadFac.create(uploadType)
       // 创建一个dom元素,绑定删除事件,再push到页面
        let dom = ...
       document.body.appendChild(dom)
        uploaddatabase[id] = {
          fileName,
          fileSize,
          dom
        },
      setExternalState: function(id, obj) {
        给obj共享对象设置fileSize
        let uploadData = uploaddatabase[id]
        
      }
    }
  }
})()
  • 没有内部状态的享元
    在文件上传中,一般会提前检测来选择一种上传模式,如果浏览器支持插件就用插件,如果不支持就用flash,那么什么情况既需要插件又需要flash。
    享元模式在需要的时候要将外部状态导入内部状态。

  • 对象池
    对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是new,而是从池子里直接获取。如果对象池里没有空闲对象,则new一个,使用完成之后再进入池子等待被下次获取。

假设我们在开发一些地图应用,在搜索我家附近的时候,页面出现了2个小气泡。当我再搜索时,页面出现了6个小气泡。按照对象池的思想,第二次搜索开始前,我们只需要创建4个对象。
先定义一个获取小气泡节点的工厂,作为对象池的数组称为私有属性被包含在工厂闭包里,这个工厂有两个方法,create表示获取一个div节点,recover表示回收一个div节点。

相关文章

网友评论

      本文标题:设计模式系列二

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