js设计模式之代理模式

代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。

在某些情况下,直接访问对象不方便或者对访问对象增强一些功能,可以使用到代理模式。比如想请一个明星来办一场商业演出,一般都是联系明星的经纪人,那么经纪人就是明星的代理。

1.小明追妹子的故事

在这个故事中,假设妹子是girl对象,小明想要给妹子送花。由于妹子只有一个,就直接通过一个对象字面量表示。

class Gift {}

class Person {
  constructor(name, job) {
    this.name = name;
    this.job = job;
  }

  sendGift(target) {
    const gift = new Gift();
    target.receiveGift(this, gift);
  }
}

const girl =  {
  receiveGift(sender, gift) {
    console.log(`from ${sender.name}`, sender, gift);
  }
}

const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(girl); // from 小明 Person {name: "小明", job: "程序员"} Gift {}
复制代码

现在妹子收到礼物了,也知道了小明的姓名和工作。可是追求妹子的人很多,妹子一个人收不过来啊,这时候妹子就需要一个代理对象了,称为proxyGirl。

class Gift {}

class Person {
  constructor(name, job) {
    this.name = name;
    this.job = job;
  }

  sendGift(target) {
    const gift = new Gift();
    target.receiveGift(this, gift);
  }
}

const proxyGirl = {
  receiveGift(...args) {
    girl.receiveGift(...args);
  }
}

const girl =  {
  receiveGift(sender, gift) {
    console.log(`from ${sender.name}`, sender, gift);
  }
}

const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(proxyGirl);
复制代码

这里结果和上述一样,所做的就是增加了一个代理对象。这必然会增加一些代码,增加程序的复杂度。它的好处在于可以通过代理对象,去控制对目标对象的直接访问(见定义)。

比如在proxyGirl中去进行一些过滤。

const proxyGirl = {
  receiveGift(...args) {
    const sender = args[0];
    if(sender.job !== '程序员') {
        girl.receiveGift(...args);
    } else {
        throw sender;
    }
  }
}
复制代码

如果给妹子送礼物的是程序员,那么把他扔出去。

2.保护代理和虚拟代理

从上述例子中,可以看到两种代理方式的影子。代理对象可以帮目标对象过滤掉一些请求,比如职业是程序员的,或者没房没车的。这种代理叫做保护代理

另外,假设礼物价值不菲,在程序中new Gift也是一个代价昂贵的操作。那么我们可以把这个操作交给代理类去执行。代理类首先过滤掉不符合条件的人,然后去new Gift,这是代理类的另一种形式,叫做虚拟代理,也叫做动态代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建(类似于单例模式中的惰性单例)。

const proxyGirl = {
  receiveGift(sender) {
    const sender = args[0];
    if(sender.job !== '程序员') {
        const gift = new Gift();
        girl.receiveGift(sender, gift); // 不改变目标对象的参数
    } else {
        throw sender;
    }
  }
}
复制代码

3.虚拟代理实现图片预加载

前端开发中,直接给img设置目标src不是一个好的做法。当图片体积比较大的时候,不能第一时间显示出来,就会造成空白,这很显然不是一个好的体验。常见的做法是给图片预先设置一个loading图(或分辨率较低的原图),然后用异步的方式加载图片,加载好后再替换原图片的url。这种场景就很适合时候虚拟代理(给目标对象增加loading功能)。

const myImage = {
  setSrc: (ele, src) => {
    ele.src = src;
  }
}


const proxyImage = {
  checkEle: ele => {
    if(ele.tagName !== 'IMG') {
      throw '这个对象只能代理img标签';
    }
  },

  setSrc: (ele, src) => {
    // 初始设置为loading图片
    this.checkEle();
    // 设置loading
    ele.src = 'loading.png';
    // 图片下载好了之后替换原图的url
    const img = new Image();
    img.src = src;
    img.onload = () => {
      myImage.setSrc(ele, src);
    }
  }
}

const img = document.querySelector('.some-img');
proxyImage.setSrc(img, 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1547377781665&di=6f7dd28462a295f04da213e190728681&imgtype=0&src=http%3A%2F%2Fb.zol-img.com.cn%2Fdesk%2Fbizhi%2Fstart%2F2%2F1363857521405.jpg')

复制代码

我们通过代理对象proxyImage间接访问目标对象myImage,并添加过滤标签功能,增加loading功能。

4.代理的意义

上面的实现我们完全可以放在myImage对象中。

const myImage = {
  setSrc: (ele, src) => {
    if(ele.tagName !== 'IMG') {
      throw '这个对象只能代理img标签';
    }
    
    // 设置loading
    ele.src = 'loading.png';
    // 图片下载好了之后替换原图的url
    const img = new Image();
    img.src = src;
    img.onload = () => {
      ele.src = src;
    }
  }
}
复制代码

好像也没有什么问题。代码确实能正常工作,并达到了预期的效果。不过它违反了单一职责原则。职责被定义为“引起变化的原因”,就是说有且只有一个原因引起对象的变化。如果多个原因都能引起对象变化,那么说明这个对象承担了过多的职责,它将变得巨大,并且职责之间相互耦合,那么必将导致高耦合低内聚的设计。我们在处理其中一个职责时,有可能因为强耦合性影响到另一个职责的实现。这对于测试来说也是非常不便的。

另外,在面向对象的设计中,大多数情况下,如果违反其他任何原则,同时将违背开放封闭原则。未来,如果网速非常快,不再需要loading了,那么我们要移除loading,就必须修改myImage对象。

实际上,myImage对象中,只需要实现给img标签添加src的功能。loading功能和过滤功能只是锦上添花。如果能把这些增强功能放在另一个对象里面,自然是极好的设计。于是代理的作用在这里就体现出来了。代理增强过滤标签和loading功能,操作完成后,把请求重新交给本体myImage。

5.代理和本体接口的一致性

代理对象和本体对象的接口(参数)应该保持一致。 上述例子中,如果不需要增强功能的时候,我们完全可以使用myImage对象替换proxyImage对象。在客户看来,代理对象和本体是一致的,客户并不需要知道代理和本体的区别,这样有两个好处。

  • 用户可以放心请求代理,它只关心是否得到想要的结果。
  • 在任何使用本体的地方都可以使用代理。

第二点让我想到了里氏替换原则。

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。

代理类可以看做是继承了目标类,并对其进行了增强。

此外,上面一直在谈论代理对象。注意:函数也是一个对象。

const myImage = (ele, src) => ele.src = src;
const proxyImage = (ele, src) => {
    // loading功能,省略
    myImage(ele, src);
}
复制代码

6.虚拟代理合并http请求

如果页面上有n多个checkbox,点击一个checkbox都要发送一个请求,请求携带checkbox的uniqueId参数。频繁的网络请求会带给服务器压力。最初的代码是这样的:

const postRequest = id => {
  // 发送请求操作,忽略
}

const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
  checkbox[i].onClick = function() {
    postMessage(this.unique_id);
  }
}
复制代码

那么怎样通过虚拟代理合并呢。

const postRequest = id => {
  // 发送请求操作,忽略
}

const proxyPostRequest = (() => {
  const caches = [];
  let timer;
  return id => {
    caches.push(id);
    if(timer) {
      return;
    }
    timer = setTimeout(() => {
      postRequest(caches.join(','));
      caches.length = 0;
      timer = null;
    }, 2000);
  }
})()

const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
  checkbox[i].onClick = function() {
    proxyPostRequest(this.unique_id);
  }
}
复制代码

proxyPostRequest是一个IIFE,返回一个闭包。请求不要同时发出,而是两秒后合并id,只发送一次。

proxyPostRequest应用了函数柯里化(function currying)的思想。

currying又称为部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

7.缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储。在下次计算时,如果传递进来的参数跟之前一致,则可以直接返回之前缓存的结果。这需要不含副作用的函数(如果函数中有Date.now()、Math.random()、外部变量等参与了计算,那么可能会导致缓存的结果并不正确)。

1.缓存计算结果

// 假设这里的add有巨大的计算量(狗头)
var add = (...args) => {
  return args.reduce((prev, curr) => {
    return prev += curr;
  }, 0)
}

const proxyAdd = (() => {
  const caches = [];
  return (...args) => {
    let value = caches[args.join(',')];
    if(value !== undefined) {
      return value;
    }

    return caches[args.join(',')] = add(...args);
  }
})()

proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3, 4);
复制代码

2.缓存ajax请求

实际开发中,如某些展示性的表格,分页的数据不需要重复拉取。拉取一次后,换缓存下来,下次使用可以直接访问了。react开发中可以避免重复调动action。

// action/xxx.js
const fetchPageData = (id) => (() => {
  const caches = [];
  return dispatch => {
    if(caches[id] !== undefined) {
      return;
    }

    var data = fetchxxx(id);
    if(data) {
      caches[id] = id;
      dispatch(storeData({
        type: xxx,
        data,
      }))
    }
    return data;
  }
})()
复制代码

显然这里可以使用缓存代理达到请求。

8.用高阶函数动态创建代理

上述缓存加速结果例子中,只能缓存加法的结果。如果需要缓存乘法的结果,那么又要创建一个proxyMulti的函数。这会写重复代码。可以使用工厂模式来创建缓存代理。

  return args.reduce((prev, curr) => {
    return prev += curr;
  }, 0)
}

const multi = (...args) => {
  return args.reduce((prev, curr) => {
    return prev *= curr;
  }, 1)
}

const createProxyFactory = fn => {
  const caches = [];
  return (...args) => {
    let value = caches[args.join(',')];
    if(value !== undefined) {
      return value;
    }
    return caches[args.join(',')] = fn.apply(this, args);
  }
}

const proxyAdd = createProxyFactory(add);
proxyAdd(1, 2, 3, 4);

const proxyMulti = createProxyFactory(multi);
proxyMulti(1, 2, 3, 4);

复制代码

9.其他代理模式

代理模式的变种非常多,限于篇幅以及在js的适用性,一下代理简单介绍一下。

  • 防火墙代理:控制网络资源的访问,保护主机不让“坏人”靠近。
  • 远程代理:为一个对象在不同的地址空间提供局部列表,在java中,远程代理可以是另一个虚拟机的对象。
  • 保护代理:用户对象应该有不同访问权限的情况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算引用对象呗引用的次数(怎么让我想到了getter setter)。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程。当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,dll是其典型运用场景。

10.小结

代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。

优点:

  1. 通过代理目标类,让目标类职责清晰。
  2. 代理类具有高扩展性。
  3. 智能化--缓存代理。

缺点:

  1. 由于在客户和真实对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

和其他模式的区别

1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制(文中给图片鞥家loading的时候,似乎区分不是那么明显)。

代理模式分类庞杂,在JS中最常用的是保护代理、虚拟代理和缓存代理(文中都用到了)。虽然代理模式非常有用,但不需要预先猜测是否需要使用代理,当发现不方便直接访问某个对象的时候,再编写代理也不迟。

转载于:https://juejin.im/post/5c3adff451882525c637fa91

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值