设计模式3-代理模式

代理模式

定义

当目标对象不方便直接访问或者访问者不满足要求的时候,提供一个代理对象来控制对目标对象的访问。访问者实际上访问的是代理对象。代理对象对请求做出一些处理之后,再把请求转交给目标对象。

主要解决

在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

优缺点

优点

  • 职责清晰。
  • 高扩展性。
  • 智能化。

缺点

  • 由于在客户端和真实主体之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  • 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

远程代理(主要针对Java)

远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。

Copy-on-Write 代理

定义

Copy-on-write (CoW or COW), sometimes referred to as implicit sharing or shadowing, is a resource-management technique used in computer programming to efficiently implement a “duplicate” or “copy” operation on modifiable resources.

写时复制(copy-on-write,简称 CoW 或 COW),也叫隐式共享(implicit sharing)或隐藏(shadowing),是计算机编程中的一种资源管理技术,用于高效地复制或拷贝可修改资源

If a resource is duplicated but not modified, it is not necessary to create a new resource; the resource can be shared between the copy and the original. Modifications must still create a copy, hence the technique: the copy operation is deferred to the first write. By sharing resources in this way, it is possible to significantly reduce the resource consumption of unmodified copies, while adding a small overhead to resource-modifying operations.

具体的,如果复制了一个资源但没有改动,就没必要创建这个新的资源,此时副本能够与原版共享同一资源,在修改时仍需要创建副本。因此,关键在于:将拷贝操作推迟到第一次写入的时候。通过这种方式来共享资源,能够显著减少无改动副本的资源消耗,而只是略微增加了资源修改操作的开销

实例

但是在JavaScript中,以前还不好对这种代理进行很好的描述,因为以前JavaScript并没有对对象属性进行监听的方法(实际上也只有obj.defineProperty),在ES6引入了Proxy之后,我们可以很方便的实现COW代理。下面给出简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = {
name: 'tom',
num: 123
}

let p_person = new Proxy(person, {
get: function(target, proKey, receiver){
return target[proKey]
},
set: function(target, propKey, value, receiver){
//深拷贝一个对象
let newObj = deep_clone(target)
//修改新对象的值
newObj[proKey] = value
//替换proxy对象为新对象
p_preson = newObj
}
})

上面的例子中,充分利用了JavaScript弱类型语言的特征,在最开始时将p_person赋值为Proxy对象,在执行get操作时,直接返回被访问对象的值;在执行set操作时,在对被访问对象执行深拷贝,并把p_person赋值为新对象。

但是这样存在一些问题:

  • Proxy对象同样会占用空间,如果确定目标对象大概率会被改动,并且目标对象并不是很大,建议直接deep_clone
  • Proxy对象内部的定义比较复杂,每一个对象都要重复该过程。所以我们封装一个函数,来执行这个过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let person = {
name: 'tom',
num: 123
}

function produce_p(obj){
let new_obj = new Proxy(obj, {
get: function (target, propKey, receiver) {
return target[propKey];
},
set: function (target, propKey, value, receiver) {
//深拷贝一个对象
let newObj = deep_clone(target)
//修改新对象的值
newObj[proKey] = value
//替换proxy对象为新对象
new_obj = newObj
},
})
return new_obj
}

let person_p = produce_p(person) //代理对象

这样我们就解决了第二个问题,第一个问题是无法解决的,需要我们自行解决。

保护(Protect or Access)代理

定义

保护代理模式(Access Proxy), 也叫Protect Proxy. 这种代理用于对真实对象的功能做一些访问限制, 在代理层做身份验证. 通过了验证, 才调用真实的主体对象的相应方法。

这种限制,应该分为两种:

  • 对访问来源做限制
  • 对访问内容做限制

实例

但是JavaScript中目前不能对一种进行限制(使用透明代理),我们使用ProxyAPI来实现对访问内容进行限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//加入我们要对sex进行读限制,对person进行写限制
let content = {
person: 'tom',
place: 'park',
sex: 'male'
}

let content_p = new Proxy(content, {
get: function(target, propKey, receiver){
return proKey === 'sex' ? false : target[proKey]
},
set(target, propKey, value, receiver){
proKey === 'person' ? '' : target[proKey] = value
}
})

同样的,我们也可以对content_p进行封装,创建一个工厂,以进行更好复用代码。

如果实际要对访问来源做限制,只能使用非透明的方式,即读写必须通过函数来进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let content = {
person: 'tom',
place: 'park',
sex: 'male'
}

let source1 = {
name: 'source1'
}

let source2 = {
name: 'source2'
}

function get(obj, prop){
//限制来自于source1的访问
if(this.name === 'source1' || prop === 'sex'){
return false
}else{
return obj[prop]
}
}

//模拟从source1来源下访问content
get.call(source1, content, 'sex')
//false

缓存(Cache)代理

定义

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

实例

下面是一个对乘积进行缓存的代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let mult = function{
let res = 1
for(let i = 0, len = arguments.length; i < len; i++){
res *= arguments[i]
}
return res
}

let proxy_mult = (function(){
let cache = {}
return function(){
let args = Array.prototype.join.call(arguments)
if(args in cache){
return cahe[args]
}
return cache[args] = mult.apply(this, arguments)
}
})()

同样的,我们可以对proxy_mult进行封装,创建一个工厂,来生成函数工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//计算乘积函数
let mult = function{
let res = 1
for(let i = 0, len = arguments.length; i < len; i++){
res *= arguments[i]
}
return res
}

//计算加和函数
let plus = function{
let res = 1
for(let i = 0, len = arguments.length; i < len; i++){
res += arguments[i]
}
return res
}

let proxyProxy = function(fn){
let cache = {}
return function(){
let args = Array.prototype.join.call(arguments)
if(args in cache){
return cahe[args]
}
return cache[args] = fn.apply(this, arguments)
}
}

let proxyMult = proxyProxy(mult)
let proxyPlus = proxyProxy(plus)

proxyMult(1,2,3,4) //24
proxyPlus(1,2,3,4) //10

虚拟代理

定义

虚拟代理作为创建开销大的对象的代表,经常会直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

实例

合并请求减少开销

这个用法其实和函数的节流效果一致,将多次操作合并为一次,以减少消耗,特别是网络请求。

比如文件同步时,虽然我们和设计checkbox来合并请求,但是并不是每个用户都会按我们设计的逻辑来进行操作。他们可能仍然一个一个地同步,这样会加大服务器的负担。所以我们可以把一段时间内的请求存储下来,每隔一段时间进行同步。以减少服务器的负担。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let syncFile = function(id){
//同步
}

let proxySyncFile = (function(){
let ids = []
let timer
return function(id){
ids.push(id)
if(timer){
return
}
timer = setTimeout(function(){
syncFile(ids.join())
claerTimeout(timer)
timer = null
cache.length = 0
}, 2000)
}
})()

惰性加载

这个作用就是虚拟代理最初的用法。

比如我们有一个库名叫composite,其作用是

  • 查询页面内图片的的OCR文字(picOcr)
  • 建立页面的虚拟DOM(virtualDOM)
  • 进行大数计算(bigNumberCalc)
  • 用webGL绘制一个游戏(webGLGame)

可以想象,这个库的每个功能都是比较困难,所以其代码量都比较大,并且我们并不一定会使用到其所有功能,假如picOcr是最常用的模块,我们直接加载该模块。所以我们可以将其他的模块做成懒加载,需要使用时再加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const composite = {
picOcr : function(){
//直接加载
},
virtualDOM: function(){
let vd = _load('virtualDOM')
composite.virtualDOM = vd
composite.virtualDOM(arguments)
},
bigNumberCalc: function(){
//逻辑相同
},
webGLGame: function(){
//逻辑相同
},
_load: function(name){
//加载模块
return get(name)
}
}

当然,上面的设计是最简单的模块设计方式,有很多问题,但是这不是我们讨论的重点,我们这里讨论的重点是延迟加载部分,可以看到

virtualDOM模块最初是没有被加载的,在调用的时候我们才通过网络加载,最后覆盖原始对象,下次调用时就是直接调用实际代码了。

但实际上这不是好的方案,好的延迟加载方案是:

在库被加载完成后,通过异步网络同时加载需要的模块,这样用户在调用其他模块时,也不会感觉到延迟,同时不会阻塞页面。

其他代理

在代理模式中,还有其他的模式,但是在JavaScript中并不是常用的,比如:

防火墙代理

控制网络资源的访问,保护服务器资源的安全。

远程代理

为一个对象在不同的地址空间提供局部代表。在JavaScript中,远程代理可以是另一个虚拟机中的对象。

智能引用代理

取代了简单的指针,它在访问对象的同时执行一些附加操作,比如计算一个对象被引用的次数。

ES6的Proxy

在ES6之前,JavaScript只能通过Object.defineProperty来进行数据代理。并且还无法做到对对象属性的代理监听。以至于Vue不得不设计一个Vue.prototype.set来监听对象属性的修改。但是ES6的的ProxyAPI在编程语言的层面上提供了代理,这样使得我们这一章的代理模式的实现变得极其简单。下面简单介绍其用法。

ES6提供原生的Proxy构造函数,用来生成Proxy实例。

1
let proxy = new Proxy(target, handler)

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,-

  • target参数表示所要拦截的目标对象。
  • handler参数也是一个对象,用来定制拦截行为。

如果handler没有做任何拦截为{},则直接通向原对象。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let stu = {
name: 'tom',
number: 123,
sex: 'male'
}

let handler = {
get: function(target, propKey, receiver){
if(propKey === 'sex'){
return false
}
return target[propKey]
},
set: function(target, propKey, value, receiver){
if(propKey === name){
return false
}
target[propKey] = value
}
}
let stu_p = new Proxy(stu, handler)

上面代理拦截了getset方法,拦截了获取stu对象的sex属性、设置stuname属性。

其中handler中,支持的拦截属性有13种:

  • **get(target, propKey, receiver)**:拦截对象属性的读取,比如proxy.fooproxy['foo']
  • **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • **has(target, propKey)**:拦截propKey in proxy的操作,返回一个布尔值。
  • **deleteProperty(target, propKey)**:拦截delete proxy[propKey]的操作,返回一个布尔值。
  • **ownKeys(target)**:拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • **getOwnPropertyDescriptor(target, propKey)**:拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • **defineProperty(target, propKey, propDesc)**:拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • **preventExtensions(target)**:拦截Object.preventExtensions(proxy),返回一个布尔值。
  • **getPrototypeOf(target)**:拦截Object.getPrototypeOf(proxy),返回一个对象。
  • **isExtensible(target)**:拦截Object.isExtensible(proxy),返回一个布尔值。
  • **setPrototypeOf(target, proto)**:拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

简单介绍上面的参数分别为:

  • target:目标对象,即被代理的对象

  • propKey: 操作的属性键名

  • value:操作的属性值

  • receiver:代理对象

  • propKey:属性键名

  • propDesc:属性的描述,详见Object.defineProperty()

  • proto:原型对象

Vue3中的响应式完全依赖了这个API,所以其是非常有用的。用起来也简单。还是需要深入理解。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :