代理模式
看本文之前需要有JavaScript的基础知识
代理模式是为了一个对象提供一个代用品或占位符,以便控制对它的访问
道理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理后,再把请求转交给本体对象。
一、第一个例子——小明追MM的故事
在四月一个晴朗的早晨,小明遇到了他百分百女孩,我们暂且称呼小明的女神为A。两天过后,小明决定给A送一束花来表白。刚好小明打听到A和他有一个共同的朋友B,于是内向的小明决定让B来代替自己完成送花这件事情。
不用代理模式:
var Flower = function(){}; |
接下来,我们引入代理B,即小明通过B来给A送花:
var Flower = function(){}; |
现在我们改变故事的背景设定,假设当A在心情好的时候收到花,小明表白成功的机率有60%,而当A在心情差的时候收到花,小明表白的成功率无限趋近于0.
小明跟A刚刚认识两天,还无法辨别A什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大。
但是A的朋友B却很了解A,所以小明只管把花交给B,B会监听A的心情变化,然后选择A心情好的时候把花转交给A:
var Flower = function(){}; |
二、保护代理和虚拟代理
代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉。这种代理叫做保护代理。A和B一个充当白脸,一个充当黑脸,白脸A继续保持良好的女神形象,不希望直接拒绝任何人,于是找了黑脸B来控制对A的访问。
假设现实中化的价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时再执行new Flower,这是代理的另一种模式,叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
var B = { |
保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。
三、虚拟代理图片预加载
在web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间是空白的。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。
var myImage = (function(){ |
现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的图,来提示用户图片正在加载。
var myImage = (function(){ |
四、代理的意义
不用代理的预加载图片函数实现如下:
var MyImage = (function(){ |
为了说明代理的意义,下面我们引人一个面向对象设计的原则——单一职责原则。
单一职责原则指的是,就一个类(通常也包括对象和丽数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
职责被定义为“引起变化的原因”。上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放——封闭原则。如果我们只是从网络上获取一些体积很小的图片,或者5年后的网速快到根本不再需要预加载,我们可能希望把预加载图片的这段代码从MyImage对象里删掉。这时候就不得不改动MyImage对象了。
实际上,我们需要的只是给img节点设置STC,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。
五、代理和本体接口的一致性
上一节说到,如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的别,这样做有两个好处。
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
在Java等语言中,代理和本体都需要显式地实现同一个接口,一方面接口保证了它们会拥有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用。
在JavaScrit这种动态类型语言中,我们有时通过鸭子类型来检测代理和本体是否都实现了setSrc方法,另外大多数时候甚至干跪不做检测,全部依赖程序员的自觉性,这对于程序的健壮性是有影响的。不过对于一门快速开发的脚本语言,这些影响还是在可以接受的范围内,而且我们也习惯了没有接口的世界。
另外值得一提的是,如果代理对象和本体对象都为一个两数(两数也是对象),两数必然都能被执行,则可以认为它们也具有一致的“接口”。
六、虚拟代理合并HTTP请求
先想象这样一个场景:每周我们都要写一份工作周报,周报要交给总监批阅。总监手下管理着150个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时间都花在查看邮件上面。
现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合并提炼成一份后一次性地发给总监。这样一来,总监的邮箱便清净多了。
这个例子在程序世界里很容易引起共鸣,在Web开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台备用服务器上面。
七、缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
缓存代理的例子——计算乘积
先创建一个用于求乘积的函数:
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函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。
缓存代理用于ajax异步请求
我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。
显然这里也可以引人缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。
八、其他代理模式
代理模式的变化种类非常多
- 防火墙代理:控制网络资源的访问,保护主机不让”坏人”接近。
- 远程代理:为一个对象在不同的地址空间提供局部代表,在Java中,远程代理可以是另一个虚拟机中的对象,
- 保护代理:用于对象应该有不同访问权限的情况。
- 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
- 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。