设计模式-----3.代理模式

代理模式:
一个对象不能直接引用另一个对象,需要代理对象在这两个对象中起到中介的作用。

1.代理的例子——小明追小红

小明决定给小红送一束花来表白,小明决定让小红的闺蜜小兰来代替自己完成送花这件事情。

先看看不用代理模式的情况:

var Flower = function () { };

var ming = {
  sendFlower: function (target) {
    var flower = new Flower();
    target.receiveFlower(flower);
  }
};
var hong = {
  receiveFlower: function (flower) {
    console.log('收到花 ' + flower);
  }
};
ming.sendFlower(hong);

接下来,我们引入代理 小兰,即小明通过 小兰 来给 小红送花:

var Flower = function () { };
var ming = {
   sendFlower: function (target) {
     var flower = new Flower();
     target.receiveFlower(flower);
   }
 };
 var lan = {
   receiveFlower: function (flower) {
     hong.receiveFlower(flower);
   }
 };
 var hong = {
   receiveFlower: function (flower) {
     console.log('收到花 ' + flower);
   }
 };
 ming.sendFlower(lan);

也许读者会疑惑,小明自己去送花和代理 小兰帮小明送花,二者看起来并没有本质的区别,引 入一个代理对象看起来只是把事情搞复杂了而已。

可是其实小兰起到了很重要的作用,假设当小红在心情好的时候收到花,小明表白成功的几率有 60%,而当小红在心情差的时候收到花,小明表白的成功率无限趋近于 0。

作为闺蜜的小兰就可以在合适的时间送花,来提高成功的概率了。
代码如下:

var Flower = function(){};
var ming = {
	sendFlower: function( target){
	var flower = new Flower();
	target.receiveFlower( flower ); }
};
var lan = {
	receiveFlower: function( flower ){
		hong.listenGoodMood(function(){ // 监听 小红 的好心情
			hong.receiveFlower( flower );
		}); 
	}
};
var hong = {
	receiveFlower: function( flower ){
		console.log( '收到花 ' + flower );
	},
	listenGoodMood: function( fn ){
		setTimeout(function(){ // 假设 10 秒之后 小红 的心情变好
			fn(); 
		}, 10000 );
	} 
};
xiaoming.sendFlower( lan );

2 保护代理和虚拟代理

虽然上面的只是个虚拟的例子,但我们可以从中找到两种代理模式的身影。

保护代理:
代理小兰可以帮助小红过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 小兰 处被拒绝掉。这种代理叫作保护代理。

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

var lan = {
	receiveFlower: function( flower ){
		hong.listenGoodMood(function(){  // 监听 小红 的好心情
			var flower = new Flower();   // 延迟创建 flower 对象
			hong.receiveFlower( flower );
		});
 	}
};

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

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( 'https://www.baidu.com/img/bd_logo1.png' );

我们把网速调至 5KB/s,然后通过 MyImage.setSrc 给该 img 节点设置 src,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。

现在开始引入代理对象 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.onload = function(){
		myImage.setSrc( this.src ); 
	}
	return {
		setSrc: function( src ){
			myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
			img.src = src; 
		}
	} 
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

现在我们通过 proxyImage 间接地访问 MyImage。proxyImage 控制了客户对 MyImage 的访问,并 且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为 一张本地的 loading 图片。

4. 代理的意义

也许会有疑问,不过是实现一个小小的图片预加载功能,即使不需要引入任何模式也能 办到,那么引入代理模式的好处究竟在哪里呢?下面我们先抛开代理,编写一个更常见的图片预加载函数。

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

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:// /C:/Users/svenzeng/Desktop/loading.gif';
			img.src = src; 
		}
	} 
})();
MyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

我们讲过单一责任原则,简而言之就是一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。

如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可 能会有多个。

面向对象设计鼓励将行为分布到不同的对象之中,如果一个对象承担的职责过多, 等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

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

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

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

纵观整个程序,我们并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系 统添加了新的行为。这是符合开放—封闭原则的。给 img 节点设置 src 和图片预加载这两个功能, 被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载, 那么只需要改成请求本体而不是请求代理对象即可。

5. 代理和本体接口的一致性

在上面的代码中,我们的代理对象和本体都对外提供了 setSrc 方法,在客户看来,代理对象和本体是一致的, 代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。

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

6. 虚拟代理合并http 请求

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

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

这个例子在程序世界里很容易引起共鸣,在 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 绑定点击事件,并且在点击的同时往另一台服务器同步文件:

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 并不是很复杂的操作,如果我们点的非常得快,可以预见,如此频繁的网络请求将会带来相当大的开销。

解决方案是,我们可以通过一个代理函数 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 ); 
		}
	}
};

7.缓存代理

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

7.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 函数可以继续专注于自身的职责——计算乘积,缓存的功能 是由代理对象实现的。

8. 动态创建代理

/**************** 创建缓存代理的工厂 *****************/ 
var createProxyFactory = 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 plus = function(){
 	var a = 0; 
	for ( var i = 0, l = arguments.length; i < l; i++ ){ + arguments[i];{
 		a = a + arguments[i];
	}
	return a; 
};
var proxyPlus = createProxyFactory( plus );
 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10

9.其他代理方式

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

参考资料

JavaScript设计模式与开发实践----曾探
JavaScript设计模式----张容铭

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值