代理模式

概述

代理模式(proxy pattern) 的主要处理逻辑为,构建代理对象以桥接对实际对象的访问。因此,可以在访问过程中构建附加的间接性操作如请求处理、权限校验、内务处理(housekeeping task)等,也可以为多种实际对象提供统一的接口。

《设计模式:可复用面向对象软件的基础》中的说法是:

Provide a surrogate or placeholder for another object to control access to it.

维基百科的说法是:

A proxy, in its most general form, is a class functioning as an interface to something else. A proxy is a wrapper or agent object that is being called by the client to access the real serving object behind the scenes. Use of the proxy can simply be forwarding to the real object, or can provide additional logic. In the proxy extra functionality can be provided, for example caching when operations on the real object are resource intensive, or checking preconditions before operations on the real object are invoked.

使用代理模式的场景:

  1. 远程代理(remote proxy): 为一个远程服务对象提供本地表现,通过本地方法调用远程服务。
  2. 虚代理(virtual proxy): 在代理中延迟创建一个开销很大的对象。如在图片代理对象的实现过程中,draw 方法执行前才创建实际所需的图片对象。
  3. 保护代理(protection proxy): 访问对象前执行权限校验。
  4. 智能指引(smart reference): 访问实际的对象时执行附加操作,如对引用计数,当没被引用时,释放内存;首次引用时,将持久对象装入内存等。

结构

  • Proxy: 以引用形式持有实体,接口与 Subject 相同,以便于使用代理对象替代实体,并控制对实体的访问。必要时,可以在代理对象中创建或删除实体。
    • remote proxy 负责对请求及其参数进行编码,并向远程服务器发送请求。
    • virtual proxy 负责缓存实体的附加信息,以便延迟创建实体。
    • protection proxy 负责校验请求是否有特定的访问权限。
  • Subject 定义 RealSubject, Proxy 的公共接口,因此在需要使用 RealSubject 的场景中都可以使用 Proxy 代替。
  • RealSubject 定义 Proxy 所代表的实体。

经典实现

使用代理模式为多种类型的字段提供统一的接口,在 FieldProxy 实例 setValue 方法执行过程中,我们也能添加诸如数据校验等处理函数。这里只展示代理模式实现的一种方式,而不推敲代理模式在字段处理上的意义。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
class Field {
type;// 字段类型
name;// 字段 code
title;// 字段名
required;// 字段是否必填
value;// 字段的值

constructor(props){
const { name, title, required } = props;
this.name = name;
this.title = title;
this.required = required;
}

setValue(value){
this.value = value;
}
}

// input
class Input extends Field {
type = 'input';
placeholder;
maxLength;

constructor(props){
super(props);
const { placeholder, maxLength } = props;
this.placeholder = placeholder;
this.maxLength = maxLength;
}
}

// textarea
class Textarea extends Field {
type = 'textarea';
placeholder;
maxLength;

constructor(props){
super(props);
const { placeholder, maxLength } = props;
this.placeholder = placeholder;
this.maxLength = maxLength;
}
}

// radio
class Radio extends Field {
type = 'radio';
options;

constructor(props){
super(props);
const { options } = props;
this.options = options;
}
}

// checkbox
class Checkbox extends Field {
type = 'checkbox';
options;

constructor(props){
super(props);
const { options } = props;
this.options = options;
}
}

// select
class Select extends Field {
type = 'select';
placeholder;
options;

constructor(props){
super(props);
const { options } = props;
this.placeholder = placeholder;
this.options = options;
}
}

const Fields = {
'input': Input,
'textarea': Textarea,
'radio': Radio,
'checkbox': Checkbox,
'select': Select,
}

class FieldProxy {
field;

constructor(props){
const { type, ...others } = props;
this.field = new Fields[type](others);
}

setValue(value){
this.field.setValue(value);
}
}

js中的代理模式

图片缓存

图片缓存的目的是在远程图片加载的时延过程中,预先以本地图片或占位符代替;在远程图片加载完成之后,再使用 img 节点展示实际的图片。

备注:RealSubject 和 Proxy 使用相同的接口这种情况,在 js 中,可以在自调用匿名函数实现,实例属性可使用闭包缓存。

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
// 实际加载图片
const loadImage = (function(){
let imgNode = document.createElement('img');
document.body.appendChild(imgNode);

return function(src){
imgNode.src = src;
};
})();

const handler = {
apply: function(target, thisBinding, args) {
const src = args[0];
let img = new Image;
img.src = src;
img.onLoad = function(){
target(src);
};

target('local_image.gif');
}
};

// 代理加载图片
const loadImageProxy = new Proxy(loadImage, handler);

loadImageProxy('remote_image.png');

惰性加载

跟代理图片类似,加载 js 脚本也会有一定的时延,这就会造成使用某个类库前,js 脚本还未加载完成,所使用的方法也是 undefined。这时,可使用虚拟代理模拟类库,缓存执行方法,等到 js 脚本加载完成之后,再使用缓存执行实际的方法。

曾探在《Javascript 设计模式与开发实践》一书中,以 minConsole 类库为例,按下 F2 键加载所需的 js 脚本,在此之前,使用代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const miniConsole = (function(){
let cache = [];
let handler = function(e){
if ( e.keyCode === 113 ){
let script = document.createElement('script');
script.onLoad = function(){// 加载完成后,执行缓存的待执行函数
cache.forEach(fn => fn());
}
script.src = 'miniConsole.js';
document.getElementByTagName('head')[0].appendChild(imgNode);
document.body.removeEventListener('keydown', handler);// 单次加载
};
};

document.body.addEventListener('keydown', handler, false);

// 代理对象,缓存待执行函数
return {
log(...args){
cache.push(miniConsole.bind(miniConsole, ...args));
}
}
})()

合并http请求

搜索组件每次改变值时都会调用远程接口,使用代理模式可延迟调用远程接口,以使交互请求不至频繁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function doSearch(content){
get(content);
}

let cache;
let timer = null;

const handler = {
apply: function(target, thisBinding, args) {
const content = args[0];
cache = content;

if ( timer ) return;

timer = setTimeout(function(){
doSearch(cache);
cache = null;
timer = null;
})
}
};

const doSearchProxy = new Proxy(doSearch, handler);

缓存代理

前端缓存代理包含:对于计算复杂的过程,在入参相同的情况下,可使用缓存的计算结果代替实际的执行计算;对于频繁的 ajax 调用,也可以使用缓存,避免远程调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let cache;

function complexCompute(...args){};

const handler = {
apply: function(target, thisBinding, args) {
const cacheKey = args.join(',');
if ( cache[cacheKey] ) return cache[cacheKey];

const result = complexCompute(...args);
cache[cacheKey] = result;
return result;
}
};

const complexComputeProxy = new Proxy(complexCompute, handler);

参考

[设计模式:可复用面向对象软件的基础]
[Javascript 设计模式和开发实践 - 曾探]
java-design-patterns: proxy