Skip to content

代理模式

我们来回顾上一章策略模式留下的问题:


假设公司的绩效等级分为 A,B,C,D,E 年终奖对应的绩点为 2,1.8,1.5,1.1,0.8。假设年终基数为 base,年终奖为:年终基数 * 绩点。已知绩效等级,求员工的年终奖。
js
// 绩效等级和绩点的映射关系
const levelToCredit = {
    A: 2,
    B: 1.8,
    C: 1.5,
    D: 1.1,
    E: 0.8
}

// 年终奖计算逻辑
function getsalary(base, level) {
  return levelToCredit[level] * base
}

代理模式,有点像经纪人的角色,可以完成非核心事务。假设我们要使用一个函数在页面加载一张图片,代码应该是这样的:

js
var imgNode = (function () {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()

imgNode.setSrc('https://img.com/banner.png') // 加载图片

事实上,我们调用 setSrc 的时候,会发起一个网络请求,在图片资源返回前,图片是空白的,为了避免图片返回的时候闪烁,我们为图片增加 loading 的情况。假设一开始直接修改上述代码:

js
var imgNode = (function () {
  // 真正展示图片的节点
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  // 代理节点
  var proxyImg = document.createElement('img')
  // 当代理图片加载完成的时候,把图片的链接赋值给展示节点
  proxyImg.onload = function() {
    imgNode.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      // 预先loading
      imgNode.src = 'https://img.com/loading.gif'
      // 代理节点开始请求图片数据
      proxyImg.src = src
    }
  }
})()

imgNode.setSrc('https://img.com/banner.png') // 加载图片

这样的代码相信是很多人的第一选择,包括我开始想到的方案也是这样的,那这样的代码有什么缺点呢? 在未来假设网速快到极致,这样 loading 占位的图片反而导致页面闪烁,这时候如果想把代理的方式去掉,就要修改函数的逻辑,那这样就违反了开放封闭的原则。 并且代理和创建节点,这两个职责耦合到了一个函数,也违反单一职责原则。不过大家现在可能仅仅知道我们要遵守这些原则,至于为什么,比较少人考究,也很少文章详细解释为什么。 以我个人的开发经验来说,可以这么理解:

假设某个需求里面有 A 和 B 两个功能,B 功能是帮 A 做一些事;小王同学把这两个功能都写在了函数 F 里,某一天,产品要砍掉 B 这个功能,然后这时小王离职了,小李接替他的工作来砍掉B,小李看完代码发现砍掉 B 得修改整个 F 函数,小李不仅要看懂函数 F,还要把 B 功能从 F 里面剔除,并且最后还要保证不影响 A 功能。这么一顿操作,小李感觉整个人都不好了,因为他只是想砍掉 B。 不幸的是他还要同时了解 A 功能,F 函数的代码接口以及修改。这样的开发模式在大型应用里面时非常致命的,容易导致整个项目稳定性过差,并且开发的代码容易成为相互的心智负担。

假设我们使用代理模式把 B 分离出去,回到上面的例子,首先我们是没有代理的代码,imgNode 函数负责生成一张图片:

js


var imgNode = (function () {
  // 创建展示的图片节点
  const imgNode = document.createElement('img')
  // 挂载到body
  document.body.appendChild(imgNode)
  // 暴露setSrc
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()


var proxyImg = (function(){
  // 图片节点
  let node = null
  // 创建代理节点
  const proxyImg = document.createElement('img')
  // 图片加载完成 把node设置为代理图片的src
  proxyImg.onload = function() {
    node.setSrc(this.src)
  }
  return (imgNode) => {
    return {
      setSrc: function(src) {
        // 使用代理图片节点请求
        proxyImg.src = src
        // 缓存 node
        node = imgNode
        // 把展示图片节点的图片设置为 loading
        img.setSrc('https://img.com/loading.png')
      }
  }
  }
})()

const img = proxyImg(imgNode)
img.setSrc('https://img.com/banner.png')

这里 proxyImg 这个函数完全是一个锦上添花的功能,假设未来网速快到一定程度,这种 loading 可以淘汰,那我们不需要代理也能正常使用,把 img.setSrc 换成 imgNode.setSrc 实现功能。那么代理模式有什么特点呢,首先实现了和原功能一样的 api 这样在取消使用代理模式的时候也不会因为 api 的差异性而进行大改。