第6章代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如明星都有经济人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人,经纪人会把演出的细节和报酬都谈好之后,再把合同交给明星。
代理模式的关键是,当客户不方便直接访问一个对象或不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
6.1 第一个例子-------屌丝追MM的故事
问题描述,屌丝(小明)追妹子,由于害羞没有自己直接追妹子而委托了一个双方都认识的妹子代理来完成此艰巨的任务。一天,屌丝想送妹子一束花来表白,于是他找到妹子代理,要求妹子代理帮他向MM送花表白一下。结果……(代理妹子和MM好啦……??……)
于是我们可以用代码来描述一下追妹子的过程,
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 B = {
receiveFlower:function(flower){
A.receiveFlower(flower);
}
}
xiaoming.sendFlower(B);
很显然,执行结果跟第一段代码一致,到此我们就完成了一个最简单的代理模式的编写。也许读者会疑惑、小明自己去送花和代理B帮小明送花,二者看起来并没有本质的区别,引入一个代理对象看起来是把事情搞复杂而已。
的确,此处的代理模式毫无用处,它所做的只是把请求简单地转交给本体。但不管怎样,我们开始引入了代理,这是一个不错的起点。
现在我们改变故事背景设定,假设当A在心情好的时候收到花,小明表白成功机率有60%,而当A在心情差的时候收到花,小明表白的成功率无限趋近于0。
而小明刚认识A两天,还无法辨别A什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃7天泡面换来的。
但A朋友B却很了解A,所以小明只管把花交给B,B会监听A的心情变化,所以选择A心情好的时候把花转交给A,如下:
var Flower = function(){};
var xiaoming = {
sendFlower:function(target){
var flower =newFlower();
target.receiveFlower(flower);
}
};
var B={
recevieFlower:function(flower){
A.listenGoodMood(function(){
A.receiveFlower(flower);
});
}
};
var A = {
receiveFlower:function(flower){
console.log(“收到花”+flower);
},
listenGoodMood:function(fn){
setTimeout(function(){
fn();
},10000);//10秒之后A的心情变好
}
}
xiaoming.sendFlower(B);
6.2 保护代理和虚拟代理
虽然这是一个虚拟的例子,但我们可以从中找到两种代理模式的身影,代理B可以帮A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉。这种代理叫作保护代理。A和B一个充当白脸,和个充当黑脸。白脸A继续保持良好的女神形象,不希望直接拒绝任何人,于是找了黑脸B来控制对A的访问。
假设在现实中花价格不菲,导致在程序世界里,newFlower也是一个代价最贵的操作,那么我们可以把newFlower的操作交给代理B去执行,代理B会选择在A心情好时再执行new Flower,这是代理模式的另一种模式,叫作虚拟代理。虚拟代理会把开销很大的对象,延迟到真正需要它的时候才去创建。
var B = {
receiveFlower =function(flower){
A.listenGoodMood(function(){
var flower = new Flower();
A.receiveFlower(flower);
});
}
}
保护代理用于控制不同权限的对象对目标对象的访问,但在javascript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理最常用的一种代理模式。
6.3 虚拟代理实现图片预加载
在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(‘http://imgcache.qq.com/music/photo/k/xxx.jpg’);
如果网速很慢的情况下,在图片没有下载完之前,页面上会出现一块儿空白。
现在工始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,将出现一个占位的loading.gif,来提示用户图片正在加载。
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.οnlοad=function(){
myImage.setSrc(this.src);
}
return {
setSrc:function(src){
myImage.setSrc(‘file:///c:/loading.gif’);
img.src = src;
}
}
})()
proxyImage.setSrc(‘http://imgcache.qq.com/music/photo/k/xxx.jpg’);
现在我们通过proxyImage间接地访问MyImage。proxyImage控制客户对MyImage的访问且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把img节点的src设置为一张本地loading图片。
6.4 代理的意义
也许读者会有疑问,不过是实现一个小小的图片预加载的功能,即使不需要引入任何模式也能办到,那么引入代理模式的好处究竟在哪里呢,下面我们来看一个更常见的图片预加载函数
不用代理的预加载图片函数
var MyImage = (function(){
var imgNode =document.createElement(‘img’);
document.body.appendChild(imgNode);
var img = new Image;
img.οnlοad=function(){
imgNode.src = img.src;
}
return {
setSrc:function(){
imgNode.src = ‘file://c:/loading.gif’;
img.src = src;
}
}
})();
MyImage.setSrc(“http://wwww…..”);
为了说明代理的意义:下面引入一个面向对象设计的原则—单一职责原则
它指的是:当一个类(通常包括对象和函数)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这处耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
职责被定义为:引起变化的原因。上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原责,同时将违反开放封闭原则。如果我们只是从网络上获取一些停机体积很小的图片,或者5年后的网速快到根本不需要预加载,我们可能希望把预加载操作这段代码从MyImage对象里删掉。这时候就不得不改动MyImage对象了。
实际上,我们需要的只是给img节点设置src,预加载图片只是一个锦上添花功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。
纵观整个程序,我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新行为。这是符合开放—封闭原则。
6.5 代理和本体接口的一致性
上一节说到,如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。
l 用户可以放心地请求代理,他只关心是否得到想要的结果
l 在任何使用本体的地方都可以替换成使用代理
在java等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用。
在javascript这种动态类型语言中,我们有时通过鸭子类型来检测代理和本体是否都实现了setSrc方法,另外大多数时候甚至干脆不做检测,全部依赖程序员的自觉性,这对于程序的健壮性是有影响的。
6.6 虚拟代理合并http请求
先想一个这样的场景:每周我们都要写一份工作周报,周报要交给总监批阅。总监手下管理着150个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时间都花在查看邮件上面。
现在我们把周报发送给各自的组长,组长作为代理,把组内的成员的周报合并提炼一份后一次性地发给总监。这样一来,总监的邮箱便清净多了。
如:web开发中也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台服务器上面。
接下来,给这些checkbox绑定点击事件,并且在点击的同时往另一台服务器同步文件
var synchronousFile = function(id){
console.log(‘开始同步文件,id为”+id);
}
var checkbox =document.getElementByTagName(‘input’);
for(var i=0,c;c=checkbox[i++];{
c.onclick =function(){
if(this.checked===true){
synchronousFile(this.id);
}
}//end click func
}
当我们选中3个checkbox的时候,依次往服务器发送3次同步文件的请求。问题是点的很快,频繁的网络请求将会带来相当大的开销。
解决方案是, 我们可以通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待2秒之内需要同步的文件ID打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大的副作用,却大大减轻服务器的压力。
var synchronousFile = function(id){
console.log(‘开始同步文件,id为”+id);
}
var proxySynchronousFile = (function(){
var cache=[],timer;
return function(id){
cache.push(id);
if(timer)return;
timer = setTimeout(function(){
synchronousFile(cache.join(‘,’));
clearTimeout(timer);
timer = null;
cache.length=0; //清空ID集合
},2000) //2秒发一次
}
})();
var checkbox =document.getElementByTagName(‘input’);
for(var i=0,c;c=checkbox[i++];{
c.onclick =function(){
if(this.checked===true){
proxySynchronousFile (this.id);
}
}
}
6.7 虚拟代理在惰性加载中的应用
作者曾经写过一个mini控制台的开源项目miniConsole.js,这个控制台可以帮助开发者在IE浏览器上进行一些简单的调试工作:
调用方式,miniConsole.log(1);
这句话会在页面中创建一个div,并且把log显示在div里面
miniConsole.js的代码量大根有1000行左右,也许我们并不想一开始就加载这么大的文件,因为也许并不是每个用户都需要打印log。我们希望在有必要的时候才开始加载它,比如当用户按下F2来主动唤出控制台的时候。
在miniConsole.js加载之前,为了能够让用户正常地使用里面的api,通常我们的解决方案是用一个占位的miniConsole代理对象来给用户提前使用,这个代理对象提供给用户的接口,跟实际的miniConsole是一亲的
用户使用这个代理对象来打印log的时候,并不会真正在控制台内打印日志,更不会在页面中创建任何DOM节点,即使我们想这样做也无能为力,因为真正的miniConsole.js还没有被加载,于是我们可以把打印log的请求都包裹在一个函数里面,这个包装了请求的函数就相当于其它语言中命令模式中的Command对象。随后这些函数将全部被放到缓存队列中,这些逻辑在miniConsole代理对象中完成实现的。等用户按下F2唤出控制台的时候,才开始加载真正的miniConsole.js的代码,加载完成之后将遍历miniConsole代理对象中的缓存函数队列,同时依次执行它们。
未加载真正的miniConsole.js之前的代码如下
var cache = [];
var miniConsole = {
log:function(){
varargs = arguments;
cache.push(function(){
returnminiConsole.log.apply(miniConsole,args);
})
}
};
miniConsole.log(1);
当用户按下F2时,开始加载真正的miniConsole.js代码如下
var handler = function(ev){
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.addEventListener(‘keydown’,handler,false);
//miniConsole.js代码:
miniConsole = {…} //略
当然我们要保证在F2被重复按下时候,miniConsole.js只被加载一次,另外我们整理一下miniConsole代理对象的代码,使它成为一个标准的虚拟代理对象
var miniConsole = (function(){
var cache = [];
var handler =function(ev){
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);//仅加载一次
}
};
document.body.addEventListener(‘keydown’,handler,false);
return {
log:function(){
var args = arguments;
cache.push(function(){
return miniConsole.log.apply(miniConsole,args);
});
}
}
})();
//miniConsole.js略
6.8 缓存代理
缓存代理可能为一些开销在的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面的运算结果。
如:
var mult = function(){
console.log(‘开始计算乘积’);
var a = 1;
for(vari=0,l=arguments.length;i<l;i++){
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 incache){
return cache[args];
}
return cache[args] = mult.apply(this,arguments);
}
})();
proxyMult (1,2,3,4);// 24
proxyMult (1,2,3,4);// 24
6.8.2 缓存代理用于ajax异步请求数据
分页缓存
6.9用高阶函数动态创建代理
能过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘法、加法、减法等创建缓存代理,如下:
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=0;
for(var i=0,l=arguments.length;i<l;i++){
a=a+arguments[i];
}
return a;
})();
//创建缓存代理工厂
var cateProxyFactory = function(fn){
var cache = {};
return function(){
var args =Array.prototype.join.call(arguments,’,’);
if(args in cache){
return cache[args];
}
return cache[args] = fn.apply(this,arguments);
}
}
var proxyMult = createProxyFactory(mult);
var proxyPlus = createProxyFactory(plus);
alert(proxyMult(1,2,3,4)); //24
alert(proxyMult(1,2,3,4)); //24
alert(proxyPlus(1,2,3,4)); //10
alert(proxyPlus(1,2,3,4)); //10