代理模式

108 阅读6分钟

代理模式

概念

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

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

保护代理和虚拟代理

保护代理

代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大或者不符合条件,这种请求就可以直接在代理B处被拒绝掉,保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理。

虚拟代理

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

虚拟代理实现图片预加载

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

首先创建一个普通的本体对象,这个对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口,外界调用这个接口,便可以给img标签设置src属性;

			let myImage = (function() {
				let imgNode = document.createElement('img')
				document.body.appendChild(imgNode);
				return {
					setSrc: function(src) {
						console.log('-');
						imgNode.src = src
					}
				}
			})()
			myImage.setSrc(
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fnimg.ws.126.net%2F%3Furl%3Dhttp%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0628%2F29618e08j00qverj7001ec000ci00loc.jpg%26thumbnail%3D650x2147483647%26quality%3D80%26type%3Djpg&refer=http%3A%2F%2Fnimg.ws.126.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1643463169&t=192cf5b5062b6ccfc503965a1f3b8965'
			)

把网速调低后,可以看到在图片被加载好之前,页面中有一段长长的空白时间

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

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("C:/Users/CHu/Desktop/qw.jpg");
						img.src = src
					}
				}
			})()

			proxyImage.setSrc(
				'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fnimg.ws.126.net%2F%3Furl%3Dhttp%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0628%2F29618e08j00qverj7001ec000ci00loc.jpg%26thumbnail%3D650x2147483647%26quality%3D80%26type%3Djpg&refer=http%3A%2F%2Fnimg.ws.126.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1643463169&t=192cf5b5062b6ccfc503965a1f3b8965'
			)

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

单一原则 && 代理意义

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

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

纵观整个程序,我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统加了新的行为。这是符合开放-封闭原则的。给img节点设置src和图片预加载这两个功能,被隔离在两个对象里,他们可以各自变化而不影响对方。

虚拟代理合并HTTP请求

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

现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合并提炼成一份后一次性地发给总监。

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

	<input type="checkbox" id="1">
		<input type="checkbox" id="2">
		<input type="checkbox" id="3">
		<input type="checkbox" id="4">
		<input type="checkbox" id="5">
		<input type="checkbox" id="6">
		<input type="checkbox" id="7">
		<input type="checkbox" id="8">
		<input type="checkbox" id="9">
		<script type="text/javascript">
			let synchronousFile = function(id) {
				console.log("开始同步文件,id为", id);
			}
			let checkbox = document.getElementsByTagName('input');
			for (let i = 0, node; node = checkbox[i++];) {
				node.onclick = function() {
					if (this.checked === true) {
						synchronousFile(this.id)
					}
				}
			}
		</script>

当我们选中3个chenckbox的时候,依次往服务器发送了3次同步文件的请求。而点击一个checkbox并不复杂的操作,如果一秒钟之内点击多个chenckbox。可以预见,如此频繁的网络请求将会带来相当大的开销

解决方案,可以通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发送给服务器,比如等待2s后才把2s之内需要同步的文件ID打包发给服务器,如果不是对实时性要求非常高的系统,2s的延迟不会带来太大的副作用,却能大大减轻服务器的压力,做一个节流。

缓存代理

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

缓存代理的列子 --- 计算乘积

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

			let mult = function() {
				console.log("开始计算乘积");
				let a = 1;
				for (let i = 0, l = arguments.length; i < l; i++) {
					a *= arguments[i];
				}
				return a
			}
			// 加入缓存代理函数
			let proxyMult = (function() {
				let cache = {};
				return function() {
					let args = Array.prototype.join.call(arguments, ',');
					console.log(args);
					if (args in cache) {
						return cache[args]
					}
					return cache[args] = mult.apply(this, arguments)
				}
			})()
			console.log(proxyMult(1, 2, 3, 4, 5, 6));
			console.log(proxyMult(1, 2, 3, 4, 5, 6));

用高阶函数动态创建代理

通过传入高阶函数这种更加灵活,可以为各种计算方法创建缓存代理。现在这些计算方法被当做参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘积、加法、减法等创建缓存代理

			/**计算乘积**/
			let mult = function() {
				let a = 1;
				for (let i = 0; i < arguments.length; i++) {
					a *= arguments[i];
				}
				return a
			}
			/**计算加和**/
			let plus = function() {
				let a = 0;
				for (let i = 0; i < arguments.length; i++) {
					a += arguments[i]
				}
				return a
			}
			/**创建缓存代理的工厂**/
			let creatrProxuFactory = function(fn) {
				let cache = {};
				return function() {
					let args = Array.prototype.join.call(arguments, ',')
					if (args in cache) {
						return cache[args];
					}
					return cache[args] = fn.apply(this, arguments)
				}
			}
			let proxyMult = creatrProxuFactory(mult),
				proxyPlus = creatrProxuFactory(plus);

			console.log(proxyMult(1, 2, 3, 4, 5, 6, 7, 8));
			console.log(proxyMult(1, 2, 3, 4, 5, 6, 7, 8));
			console.log(proxyPlus(1, 2, 3, 4, 5, 6, 7, 8));
			console.log(proxyPlus(1, 2, 3, 4, 5, 6, 7, 8));