说透设计模式-代理模式与Proxy

838 阅读8分钟

什么是代理模式

代理模式在生活中非常的常见,比如你想卖房子有房产代理人,明星有经纪人可以代理他们的一些事物,外卖小哥也在商家和你之间作为一种代理人,把外卖送到你的手上...

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 如图:

实现一个简单的设计模式 -李四追女神的故事

四月是春花烂漫的季节,张楚也唱到孤独的人是可耻的,这不在一个明媚的早晨,李四在公园里遇见了让他小鹿乱撞的女孩,他立马就走不动路了,于是他鼓足勇气上去搭讪要微信,迎接他的确是一顿大耳刮子。

class WxNumber {...}
class LeeSi {
    askWx(target) {
        const res = target.getRequest()
        console.log(res)
    }
}
class Beauty {
    getRequest() {
        return '一顿大耳刮子'
    }
}

const lee = new LeeSi()
lee.askWx(new Beauty())

第一次的求爱不成功,但是李四并不气馁,因为他是一个痴情种子。他多方面打听到原来他的一个好朋友张代丽原来是女神的闺蜜。于是他请张代丽出马,帮他要微信号。

class WxNumber {...}
const wxNum = new WxNumber()
class LeeSi {
    askWx(target) {
        const res = target.getRequest()
        console.log(res)
    }
}
class ZhangProxy {
    getRequest() {
        const beauty = new Beauty()
        return beauty.getRequest()
    }
}
class Beauty {
    getRequest() {
        return wxNum
    }
}
const lee = new LeeSi()
lee.askWx(new ZhangProxy())

就这样李四终于得到了女神的微信。

虽然是一个简单的例子,但是我们可以从中得到两种代理模式的身影,保护代理和虚拟代理。

  • 保护代理:虽然李四是张代丽的朋友,可若是李四是一个渣男,张代丽也不会帮李四要自己闺蜜的微信号。所以就过滤了一部门渣男的请求。
  • 虚拟代理:虚拟代理把一些开销很大的对象,延迟到请求的时候才开始创建。比如这样:
class Beauty {
    getRequest() {
        return new WxNumber()
    }
}

再来看一个例子:

虚拟代理模式实现图片预加载

在一些网站中,有时候网速不好的时候,网页中的图片会在打开的时候出现一段时间的白屏,这时候我们一般会通过预加载的技术,先在图片的位置放置一张loading图,等到图片加载好了的时候,再将图片显示出来。

我们先来实现一个本体类

class MyImg {
    constructor() {
        this.imgNode = document.createElement('img')
    }
    addImgNode() {
        document.body.appendChild(this.imgNode)
    }
    setSrc(src) {
        this.imgNode.src = src
        this.addImgNode()
    }
}

然后我们引入一个代理对象ProxyImg,通过这个代理对象,在图片被真正加载好之前,页面将出现一张占位图来告诉用户正在加载。

class ProxyImg {
    constructor() {
        this.myImg = new MyImg()
        this.img = new Image
        this.src = null
        this.img.onload = () => {
            this.myImg.setSrc(this.src)
        }
    }
    setSrc(src) {
        this.MyImg.setSrc('xxx/xxx/aa.gif')
        this.img.src = src
        this.src = src
    }
}

现在我们通过ProxyImg间接地访问MyImg, ProxyImg控制了客户对MyImg的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img节点的src设置为一张本地的loading图片。

也许有人就会说,不过是一个小小的图片预加载的功能,即使不使用任何的设计模式,我分分钟手撸一个出来。那么代理模式的作用到底体现在什么地方呢?

我觉得的是一个面向对象设计的原则:单一职责原则

单一职责原则指的是,就一个类而言,应该仅有一个 引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个

比如我们不用代理实现一个图片预加载的类:

class MyImg {
    constructor() {
        this.imgNode = document.createElement('img')
        this.img = new Image
        this.img.onload = this.loadImg()
        this.src = null
    }
    loadImg() {
        this.imgNode.src = this.src
    }
    addImgNode() {
        document.body.appendChild(this.imgNode)
    }
    setSrc(src) {
        this.imgNode.src = 'xxx/xxx/aa.gif'
        this.img.src = src
        this.src = src
        this.addImgNode()
    }
}

这段代码除了给img节点设置src外,还要负责预加载图片。当我们处理其中一个职责的时候,这就有可能因为其强耦合性影响另一个职责的实现。

还有一种情况,一些别的什么原因,可能5年后网速已经快的上天了,我们可以不用再进行代理了,那我们就直接去掉代理这一层就可以,也不用再去改MyImg这个类,这就又符合开放-封闭的原则了。

从这几个例子中我们可以看到一个规律,那就是代理暴露的方法名,和本体暴露的方法名是一致的。代理接收请求的过程对于用户来说是透明的,用户并不知道这其中的区别,这样做也就可以做到在使用本体的地方都可以替换成使用代理。

缓存代理

缓存代理可以为一些开销比较大的运算结果提供暂时的缓存,下次运算的时候,如果传递进来的参数和之前一致,则可以直接返回前面存储的运算结果。

我们先来实现一个用于求乘积的类,然后加入缓存代理

class Mult {
  cal() {
    console.log('开始计算')
    const res = [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
    return res
  }
}

class ProxyMult {
  static cache = {}
  constructor() {
    this.mult = new Mult()
  }
  cal() {
    let args = [].join.call(arguments, ',')
    if (args in ProxyMult.cache) {
      return ProxyMult.cache[args]
    }
    return ProxyMult.cache[args] = this.mult.cal.apply(this, arguments)
  }
}
const proxyMult = new ProxyMult()

console.log(proxyMult.cal(1,2,3,4))
console.log(proxyMult.cal(1,2,3,5))
console.log(proxyMult.cal(1,2,3,4))

可以很清楚的看到Mult类中的cal方法只执行了两次,所以缓存生效。

用高阶函数动态创建代理

我们写代码的过程要时刻问问自己,什么是一直在变的,什么是不变的。变化的我们尽量遵循单一职责的原则实现分别的逻辑,不变的部分我们争取封装起来,让他遵循开放-封闭原则。

开放封闭原则说的是对扩展开放,对修改封闭

所以我们可以通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理,所以计算方法就是可变的。 我们再来创建一个计算加和的类和创建缓存代理的工厂


class Mult {
  constructor() {
    this.name = 'mult'
  }
  cal() {
    console.log('开始计算Mult')
    return [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
  }
}

class Plus {
  constructor() {
    this.name = 'plus'
  }
  cal() {
      console.log('开始计算Plus')
      return [].reduce.call(arguments, ((cur, next) => {
          return cur + next 
      }), 0)
  }
}

class CreateProxyFactory {
  static cache = {}
  constructor(fn) {
    this.fn = new fn()
    console.log(this.fn)
  }
  cal() {
    const args = [].join.call(arguments, `,${this.fn.name}`)
    if (args in CreateProxyFactory.cache) {
      return CreateProxyFactory.cache[args]
    }
    return CreateProxyFactory.cache[args] = this.fn.cal.apply(this, arguments)
  }
}


const proxyMult = new CreateProxyFactory(Mult)
const ProxyPlus = new CreateProxyFactory(Plus)

console.log(proxyMult.cal(1,2,3,4))
console.log(proxyMult.cal(1,2,3,4))
console.log(ProxyPlus.cal(1,2,3,4))
console.log(ProxyPlus.cal(1,2,3,4))

代理模式有很多种类,但是在js中的适用性都不太高,有兴趣的可以单独去找资料拿来学习。

  • 防火墙代理
  • 远程代理
  • 智能引用代理
  • 写时复制代理

Proxy

下面我们来说说ES6新加的这个Api:Proxy,光看名字我们就能想到为什么写代理模式的时候也要讲一下这个Proxy。

在MDN上对于Proxy的解释是:

 Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

首先它的语法是:

let p = new Proxy(target, handler)

分别解释一下:

  • target 是你要代理的对象.它可以是JavaScript中的任何合法对象.如: (数组, 对象, 函数等等)
  • handler是你要自定义操作方法的一个集合.
  • p 是一个被代理后的新对象,它拥有target的一切属性和方法.只不过其行为和结果是在handler中自定义的,当然它也可以为target添加属性.

在我们正式介绍 Proxy 之前,建议你对 Reflect 有一定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect

Dont BB, Show me Code

const cat = {
  color: 'yellow',
  age: 3,
  isGirl: true
}

const handle = {
  get(target, key, value) {
      if (key === 'age') {
          console.log(`I'm ${target[key]}`)
      }
      return Reflect.get(target, key, value)
  },
  set(target, key, value) {
      if (key === 'isGirl') {
        console.log(`I don't want to be transgender`)
        return Reflect.set(target, key, `I don't want to be transgender`)
      }
      return Reflect.set(target, key, value)
  }
}

const newCat = new Proxy(cat, handle)
newCat.age  // I'm 3
newCat.isGirl = false // I don't want to be transgender
newCat.age = 6
console.log(newCat)

什么在handler,定义get和set这两个函数名之后就代理对象上的get和set操作了呢? 实际上handler本身就是ES6所新设计的一个对象.它的作用就是用来自定义代理对象的各种可代理操作。它本身一共有13中方法,每种方法都可以代理一种操作.其13种方法如下:

handler.getPrototypeOf()

// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。

handler.setPrototypeOf()

// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。

handler.isExtensible()

// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。

handler.preventExtensions()

// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。

handler.getOwnPropertyDescriptor()

// 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。

handler.defineProperty()

// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。

handler.has()

// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。

handler.get()

// 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。

handler.set()

// 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。

handler.deleteProperty()

// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。

handler.ownKeys()

// 在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。

handler.apply()

// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。

handler.construct()

// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。

我把把这些方法类似的理解为一些钩子函数,有些方法还是挺好玩的,大家可以仔细研究一下。

比如我们可以把上面的缓存代理换成Proxy的方式来实现:

class Mult {
  cal() {
    console.log('开始计算Mult')
    return [].reduce.call(arguments, ((cur, next) => {
      return cur*next
    }), 1)
  }
}

const target = new Mult()
const newproxy = new Proxy(target.cal, {
  apply(target, key, value) {
    target.cache = target.cache || {}
    let args = [].join.call(value, ',')
    if (args in target.cache) {
      return target.cache[args]
    }
    return target.cache[args] = target.apply(this, value)
  }
})
console.log(newproxy(1,2,3,4))
console.log(newproxy(1,2,3,4))
console.log(newproxy(1,2,3,4,5))
console.log(newproxy)

可以看到也能实现同样的功能,Proxy通过组合使用可以实现各种各样的功能,当然我列出了几种比较常见的

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

大家可以自己去研究一下,篇幅有限,不再赘述。

如果有不对或者模糊的地方,欢迎大家指正,感谢阅读。