定义
代理模式是为一个对象提供一个占位符,以便控制对它的访问。
描述
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。
代理模式的关键是当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
应用
下面从小明送花追女孩的案例来熟悉代理模式的结构,先看看不用代理模式的情况:
class Flower{
constructor(name){
this.name = name
}
}
let xiaoming = {
sendFlower: function( target ){
let flower = new Flower('玫瑰花')
target.receiveFlower( flower )
}
}
let girl = {
receiveFlower: function( flower ){
console.log( '收到' + flower.name )
}
};
xiaoming.sendFlower( girl )
引入女孩的闺蜜作为代理者:
class Flower {
constructor(name) {
this.name = name
}
}
let xiaoming = {
sendFlower: function (target) {
let flower = new Flower('玫瑰花')
target.receiveFlower(flower)
}
}
let closeFriend = {
receiveFlower(flower) {
girl.listenGoodMood(() => {
girl.receiveFlower(flower)
})
}
}
let girl = {
receiveFlower: function (flower) {
console.log('收到' + flower.name)
},
listenGoodMood: function (fn) {
setTimeout(() => {fn()}, 10000)
}
}
xiaoming.sendFlower(closeFriend)
现在改变故事的背景设定,假设当girl在心情好的时候收到花,小明表白成功的几率有60%,而当girl在心情差的时候收到花,小明表白的成功率无限趋近于0。
通过这个案例,我们可以看到代理模式的价值。小明跟girl刚刚认识两天,还无法辨别girl什么时候心情好。如果不合时宜地把花送给girl,花被直接扔掉的可能性很大。但是girl的closeFriend却很了解girl,所以小明只管把花交给closedFriend,closeFriend会监听girl的心情变化,然后选择girl心情好的时候把花转交给gril。
虽然这只是个虚拟的例子,但可以从中找到两种代理模式的身影。girl的闺蜜可以帮组girl过滤掉一些请求,比如送花的人年龄太大、长得太丑、太穷等,这种请求就可以直接在代理处被拒绝掉,这种代理叫做保护代理。另外,现实中的花价值不菲,程序世界里,new Flower()
也是一个代价昂贵的操作。把new Flower()
的操作交给代理closeFriend去执行,代理会选择在girl心情好时再执行new Flower()
,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下:
let closedFriend = {
receiveFlower() {
girl.listenGoodMood(() => {
let flower = new Flower('玫瑰花')
girl.receiveFlower(flower)
})
}
}
图片预加载
在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。
下面来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给该img标签设置。
let myImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
return {
setSrc: function( src ){
imgNode.src = src
}
}
})()
myImage.setSrc( 'coming.png' )
设置网速很差的情况下,通过myImage,setSrc给该img元素设置src,可以看出,在图片被加载好之前,页面有很长的空白时间,用户体验很不好。
现在引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的loading.gif,来提示用户图片正在加载。
let myImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
return {
setSrc: function( src ){
imgNode.src = src
}
}
})()
let proxyImage = (function(){
let img = new Image
img.onload = function(){
myImage.setSrc( this.src )
}
return {
setSrc: function( src ){
myImage.setSrc( 'loading.gif' )
img.src = src
}
}
})()
proxyImage.setSrc( 'coming.png' )
代理模式的意义
也许读者有疑问,不过是实现一个小小的图片预加载功能,及时不引入任何模式也能办到,那么引入代理模式的好处究竟在哪里?不使用代理,则图片预加载的函数实现代码如下:
let MyImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
let img = new Image
img.onload = function(){
imgNode.src = img.src
}
return {
setSrc: function( src ){
imgNode.src = 'loading.gif'
img.src = src
}
}
})()
MyImage.setSrc( 'coming.png' )
这就要讲到面向对象设计的原则——单一职责原则。
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
职责被定义为“引起变化的原因”。上面代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放——封闭原则。如果只是从网络上获取一些体积很小的图片,或者5年后的网速快到根本不再需要预加载,可能希望把预加载图片的这段代码从MyImage对象里删掉。这时候就不得不改动 MyImage 对象了。实际上,需要的只是给img节点设置src,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体MyImage。
纵观整个程序,并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放——封闭原则的。给img节点设置 src 和图片预加载这两个功能, 被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天不再需要预加载, 那么只需要改成请求本体而不是请求代理对象即可。
代理和本体接口的一致性
代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的, 代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处:1、用户可以放心地请求代理,只关心是否能得到想要的结果;2、在任何使用本体的地方都可以替换成使用代理。
虚拟代理合并HTTP请求
在Web开发中,也许最大的开销就是网络请求。假设在做一个文件同步的功能,选中一个checkbox时,它对应的文件就会被同步到另外一台备用服务器上面。
首先,在页面中放置好这些checkbox节点:
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
</body>
接下来,给这些checkbox绑定点击事件,并且在点击的同时往另一台服务器同步文件。
let synchronousFile = function( id ){
console.log( '开始同步文件,id 为: ' + id )
}
let checkboxs = document.getElementsByTagName( 'input' );
for ( let i = 0, c; c = checkboxs[ i++ ]; ){
c.onclick = function(){
if ( this.checked === true ){
synchronousFile( this.id )
}
}
}
当选中3个checkbox的时候,依次往服务器发送了3次同步文件的请求。可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案是可以通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发送给服务器。比如等待2秒之后才把这2秒之内需要同步的文件ID打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
let synchronousFile = function( id ){
console.log( '开始同步文件,id 为: ' + id )
}
let proxySynchronousFile = (function(){
let 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 )
}
})()
let checkboxs = document.getElementsByTagName( 'input' )
for ( let i = 0, c; c = checkboxs[ i++ ] ){
c.onclick = function(){
if ( this.checked === true ){
proxySynchronousFile( this.id )
}
}
}
缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
下面是一个计算乘积的例子,先创建一个用于求乘积的函数:
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
然后加入缓存代理函数:
let proxyMult = (function(){
let cache = {}
return function(){
let 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函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。
在项目中常常遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。
其他代理模式
代理模式的变体种类非常多,还包括以下几种:
1. 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
2.远程代理:为一个对象在不同的地址空间提供局部代表。
3. 保护代理:用于对象应该有不同访问权限的情况。
4. 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
5、写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。
小结
代理模式包括许多小分类,在javascript开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但在编写业务代码时,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
参考文献
《JavaScript设计模式与开发实践》