代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
先从一个小例子开始熟悉代理模式的结构:
小明想给小红送一束花,刚好小明和小红(A)有一个共同朋友小丽(B),于是内向的小名决定让小丽代替自己完成送花这件事。先看看不用代理模式的情况:
var Flower = function(){}
var xiaoming = {
sendFlower:function(target){
var flower = new Flower();
target.receiveFlower(flower);
}
}
var A = {
receiveFlower:function(flower){
console.log('收到花'+flower)
}
}
xiaoming.sendFlower(A);
接下来,引入代理小丽,即小明通过B来给A送花:
var Flower = function(){}
var xiaoming = {
sendFlower:function(target){
var flower = new Flower();
target.receiveFlower(flower);
}
}
var B = {
receiveFlower:function(flower){
A.receiveFlower(flower);
}
}
var A = {
receiveFlower:function(flower){
console.log('收到花'+flower);
}
}
xiaoming.sendFlower(B);
这段代码执行结果和第一段代码一致,我们完成了一个最简单的代理模式的编写。
既然结果都一样,我们为什么要引入B让代码更复杂呢?
现在我们改变一下故事的背景设定,假设当A在心情好的时候收到花,小明表白成功的几率有60%,而当A在心情差的时候收到花,小明表白的成功率是0。此时小明还无法辨别A什么时候心情好,所以需要找很了解A的B来监听A的心情变化,然后选择A心情好的时候把花转交给A:
var Flower = function(){}
var xiaoming = {
sendFlower:function(target){
var flower = new Flower();
target.receiveFlower(flower);
}
}
var B = {
receiveFlower:function(flower){
A.listenGoodMood(function(){//监听A的好心情
A.receiveFlower(flower);
})
}
}
var A = {
receiveFlower:function(flower){
console.log('收到花'+flower);
},
listenGoodMood:function(fn){
setTimeout(function(){//假设10秒之后A的心情变好
fn();
},10000)
}
}
xiaoming.sendFlower(B);
上面的小例子中可以找到两种代理模式的身影。代理B可以帮助A过滤掉一些请求,不好的请求可以直接在代理B处被拒绝掉,这种代理叫作保护代理。另外,假设现实中的花价格不菲,导致在程序世界里,new Flower
也是一个代价昂贵的操作,那么我们就可以把昂贵的操作交给代理B去执行,B会在A心情好的时候再执行new Flower
,这个叫做虚拟代理,虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下:
var B = {
receiveFlower:function(flower){
A.listenGoodMood(function(){//监听A的好心情
var flower = new Flower();//延迟创建flower对象
A.receiveFlower(flower);
})
}
}
接下来看另一个真实的示例:虚拟代理实现图片预加载
在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白,常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合虚拟代理。
下面我们先来创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给该img标签设置src属性:
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc:function(src){
imgNode.src = src
}
}
})()
//百度上随便找了一张图片
myImage.setSrc("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0827%2Fc0c92bd51bb72e6d12d5b877dce338e8.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1649343899&t=bd5cf00dc3a715b546d5cc65edc31bad")
把网速调慢,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。
现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中会出现一张loading图,来提示图片正在加载
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc:function(src){
imgNode.src = src
}
}
})()
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc(this.src)
}
return {
setSrc:function(src){
myImage.setSrc("./img/loading.gif");
img.src = src;
}
}
})()
proxyImage.setSrc("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0827%2Fc0c92bd51bb72e6d12d5b877dce338e8.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1649343899&t=bd5cf00dc3a715b546d5cc65edc31bad")
现在我们通过proxyImage间接地访问myImage,proxyImage控制了客户对myImage的访问,并且再次过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img节点的src设置为一张本地的loading图片。
代理的意义
可能我们会有疑惑,为什么一个小小的图片预加载功能,即使不需要引入任何模式也能办到,那么引入代理模式的好处究竟在哪里呢?
先来了解一个面向对象设计的原则-----单一职责原则
**单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。**如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
代理和本体接口的一致性
如果上面的例子,我们不需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户不清楚代理和本体的区别,这样做有两个好处。
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
虚拟代理合并HTTP请求
先想象一个场景:每周我们都要写一份工作周报,要交给总监批阅,如果总监管150人,那他就要看150份周报,如果我们把周报发给各自的组长,组长作为代理,把组内成员的周报合并提炼成一份一次性发给总监,那总监的工作量就减少很多了。
这个例子在程序世界里很容易引起共鸣,在web开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台备用服务器上面。
点击checkbox的同时往另一台服务器同步文件:
var synchronousFile = function(id){
console.log('开始同步文件,id为:'+id);
}
var checkbox = document.getElementsByTagName('input');
for(var i = 0,c;c=checkbox[i++];){
c.onclick = function(){
if(this.checked == true){
synchronousFile(this.id);
}
}
}
当我们选中3个checkbox的时候,依次往服务器发送了3次同步文件的请求。而点击一个checkbox并不是很复杂的操作,我有把握一秒钟之内点中4个checkbox。可以预见,如此频繁的网络请求将会带来相当大的开销。
使用一个代理函数proxySynchronousFile
来收集一段时间之内的请求,最后一次性发送给服务器。就可以解决这个问题,比如我们等待2秒之后才把这2秒之内需要同步的文件ID打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
var synchronousFile = function(id){
console.log('开始同步文件,id为:'+id);
}
var proxySynchronousFile = (function(){
var cache = [],//保存一段时间内需要同步的ID
timer;//定时器
return function(id){
cache.push(id)
if(timer){
return;
}
timer = setTimeout(function(){
synchronousFile(cache.join(','));//2秒后向本体发送需要同步的ID的集合
clearTimeout(timer);//清空定时器
timer = null;
cache.length = 0;//清空ID集合
},2000)
}
})()
var checkbox = document.getElementsByTagName('input');
for(var i = 0,c;c=checkbox[i++];){
c.onclick = function(){
if(this.checked == true){
proxySynchronousFile(this.id);
}
}
}
虚拟代理在惰性加载中的应用
有时我们不想马上加载一个js文件,比如一个控制台的开源项目miniConsole.js,我们希望只有在用户需要的时候才加载它,在js文件加载前,为了能让用户正常地使用里面的API,通常我们的解决方案是用一个占位的miniConsole代理对象来给用户提前使用,这个代理对象提供给用户的接口,跟实际的miniConsole是一样的。
我们可以把打印log的请求都包裹在一个函数里面,这个包装了请求的函数就相当于其他语言中命令模式中的Command对象,随后这些函数将全部被放到缓存队列中,这些逻辑在miniConsole代理对象中完成实现,等用户按下F2唤出控制台的时候,才开始加载真正的miniConsole.js的代码,加载完成之后将遍历miniConsole代理对象中的缓存函数队列,同时依次执行它们。
var miniConsole = (function(){
var cache = [];
var handler = function(ev){
console.log(ev.keyCode)
if(ev.keyCode === 113){
var script = document.createElement('script');
script.onload = function(){
for(var i = 0,fn;fn = cache[i++];){
fn()
}
}
script.src = 'miniConsole.js';
document.getElementsByTagName('head')[0].appendChild(script);
document.body.removeEventListener('keydown',handler,false);
}
};
document.body.addEventListener('keydown',handler,false);
return {
log:function(){
var args = arguments;
cache.push(function(){
return miniConsole.log.apply(miniConsole,args)
})
}
}
})()
miniConsole.log(1)
miniConsole.log(11112)
//miniConsole.js代码
miniConsole = {
log:function(){
console.log(Array.prototype.join.call(arguments))
}
}
缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
缓存代理的例子-----计算乘积
这里编写一个简单的求乘积的程序:
var mult = function(){
console.log("开始计算乘积");
var a = 1;
for(var i = 0,l = arguments.length;i<l;i++){
a = a * arguments[i]
}
return a;
}
现在加入缓存代理函数:
var proxyMult = (function(){
var cache = [];
return function(){
var args = Array.prototype.join.call(arguments);
if(args in cache){
console.log("调用缓存")
return cache[args];
}
return cache[args]=mult.apply(this,arguments);
}
})()
console.log(proxyMult(1,2,3,4));
console.log(proxyMult(1,2,3,4));
执行以后,可以看到第二次调用直接返回存在缓存中的结果。通过增加缓存代理的方式,mult函数可以继续专注于自身的职责。
用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘法、加法等创建缓存代理,代码如下:
// 计算乘积=====================
var mult = function(){
var a = 1;
for(var i = 0,l = arguments.length;i<l;i++){
a = a * arguments[i];
}
return a
}
// 计算加和=====================
var plus = function(){
var a = 1;
for(var i = 0,l = arguments.length;i<l;i++){
a = a + arguments[i];
}
return a
}
var createProxyFactory = function(fn){
var cache = [];
return function(){
var args = Array.prototype.join.call(arguments,',');
if(args in cache){
console.log("调用缓存")
return cache[args];
}
return cache[args] = fn.apply(this,arguments);
}
}
var proxyMult = createProxyFactory(mult),
proxyPlus = createProxyFactory(plus);
console.log(proxyMult(1,2,3,4))//24
console.log(proxyMult(1,2,3,4))//24
console.log(proxyPlus(1,2,3,4))//11
console.log(proxyPlus(1,2,3,4))//11
其他代理模式
代理模式的变体种类非常多,下面简单的介绍一下
- 防火墙代理:控制网络资源的访问,保护主机不让“坏人”接近。
- 远程代理:为一个对象在不同的地址空间提供局部代表,在Java中,远程代理可以是另一个虚拟机中的对象。
- 保护代理:用于对象应该有不同访问权限的情况。
- 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
- 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。