看本文之前需要有JavaScript的基础知识


代理模式是为了一个对象提供一个代用品或占位符,以便控制对它的访问

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

一、第一个例子——小明追MM的故事

在四月一个晴朗的早晨,小明遇到了他百分百女孩,我们暂且称呼小明的女神为A。两天过后,小明决定给A送一束花来表白。刚好小明打听到A和他有一个共同的朋友B,于是内向的小明决定让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,即小明通过B来给A送花:

var Flower = function(){};

var xiaoming = {
sendFlower: function(target){
var flower = new Flower();
target.receivelower(flower);
}
};

var B = {
receiverFlower: function(flower){
A.receiveFlower(flower);
}
};

var A = {
receiveFlower: function(flower){
console.log('收到花' + flower);
}
};

xiaoming.sendFlower(B);

现在我们改变故事的背景设定,假设当A在心情好的时候收到花,小明表白成功的机率有60%,而当A在心情差的时候收到花,小明表白的成功率无限趋近于0.

小明跟A刚刚认识两天,还无法辨别A什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大。

但是A的朋友B却很了解A,所以小明只管把花交给B,B会监听A的心情变化,然后选择A心情好的时候把花转交给A:

var Flower = function(){};

var xiaoming = {
sendFlower: function(target){
var flower = new Flower();
target.receiveFlower(flower);
}
};

var B = {
receiverFlower: function(flower){
A.listenGoodMood(function(){ // 监听A的好心情
A.receiveFlower(flower);
})
}
};

var A = {
receiverFlower: function(flower){
console.log('收到花' + flower);
},
listenGoodMood: function(fn){
setTimeout(function(){
fn();
}, 10000);
}
};

xiaoming.sendFlower(B);

二、保护代理和虚拟代理

代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉。这种代理叫做保护代理。A和B一个充当白脸,一个充当黑脸,白脸A继续保持良好的女神形象,不希望直接拒绝任何人,于是找了黑脸B来控制对A的访问。

假设现实中化的价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时再执行new Flower,这是代理的另一种模式,叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。

var B = {
receiverFlower: function(flower){
A.listenGoodMood(function(){ // 监听A的好心情
A.receiveFlower(flower);
})
}
};

保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。

三、虚拟代理图片预加载

在web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间是空白的。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。

var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);

return {
setSrc: function(src){
imgNode.src = src;
}
}
})();

myImage.setSrc('http://xxx.jpg');

现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的图,来提示用户图片正在加载。

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('file:xxx.gif');
img.src = src;
}
}
})();

proxyImage.setSrc('http://xxx.jpg');

四、代理的意义

不用代理的预加载图片函数实现如下:

var MyImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
var img = new Image;

img.onload = function(){
imgNode.src = img.src;
};

return {
setSrc: function(src){
imgNode.src = 'file:xxx.gif';
img.src = src;
}
}
})();

MyImage.setSrc('http://xxx.jpg');

为了说明代理的意义,下面我们引人一个面向对象设计的原则——单一职责原则。

单一职责原则指的是,就一个类(通常也包括对象和丽数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

职责被定义为“引起变化的原因”。上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。

另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放——封闭原则。如果我们只是从网络上获取一些体积很小的图片,或者5年后的网速快到根本不再需要预加载,我们可能希望把预加载图片的这段代码从MyImage对象里删掉。这时候就不得不改动MyImage对象了。

实际上,我们需要的只是给img节点设置STC,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。

五、代理和本体接口的一致性

上一节说到,如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的别,这样做有两个好处。

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

在Java等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用。

在JavaScrit这种动态类型语言中,我们有时通过鸭子类型来检测代理和本体是否都实现了setSrc方法,另外大多数时候甚至干跪不做检测,全部依赖程序员的自觉性,这对于程序的健壮性是有影响的。不过对于一门快速开发的脚本语言,这些影响还是在可以接受的范围内,而且我们也习惯了没有接口的世界。

另外值得一提的是,如果代理对象和本体对象都为一个两数(两数也是对象),两数必然都能被执行,则可以认为它们也具有一致的“接口”。

六、虚拟代理合并HTTP请求

先想象这样一个场景:每周我们都要写一份工作周报,周报要交给总监批阅。总监手下管理着150个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时间都花在查看邮件上面。

现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合并提炼成一份后一次性地发给总监。这样一来,总监的邮箱便清净多了。

这个例子在程序世界里很容易引起共鸣,在Web开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台备用服务器上面。

七、缓存代理

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

  1. 缓存代理的例子——计算乘积

    先创建一个用于求乘积的函数:

    var mult = function(){
    console.log('开始计算乘积');
    var a = 1;
    for(var i = 0; l = arguments.length;i < l; i++){
    a = a * arguments[i];
    }
    return a;
    };

    mult(2,3); // 6
    mult(2,3,4); // 24

    现在加入缓存代理函数:

    var proxyMult = (function(){
    var cache = {};
    return function(){
    var args = Array.prototype.join.call(arguments,',');
    if(args in cache){
    return cache[args];
    }
    return cache[args] = mult.apply(this,arguments);
    }
    })();

    proxyMult(1,2,3,4); // 24
    proxyMult(1,2,3,4); // 24

    当我们第二次调用proxyMult(1,2,3,4)的时候,本体mult函数并没有被计算,proxyMult直接返回了之前缓存好的计算结果。

    通过增加缓存代理的方式,mult函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。

  2. 缓存代理用于ajax异步请求

    我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

    显然这里也可以引人缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。

八、其他代理模式

代理模式的变化种类非常多

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