代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。
在某些情况下,直接访问对象不方便或者对访问对象增强一些功能,可以使用到代理模式。比如想请一个明星来办一场商业演出,一般都是联系明星的经纪人,那么经纪人就是明星的代理。
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、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制(文中给图片鞥家loading的时候,似乎区分不是那么明显)。
代理模式分类庞杂,在JS中最常用的是保护代理、虚拟代理和缓存代理(文中都用到了)。虽然代理模式非常有用,但不需要预先猜测是否需要使用代理,当发现不方便直接访问某个对象的时候,再编写代理也不迟。