代理 Proxy

代理模式是一种结构型设计模式,让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

代理模式的基本理念是作为一个外包装的中间层,享有控制住访问对象的权力,同时也能扩展一些功能。其核心能力在于对某一个具体的功能进行增强和补充。


为什么要使用?

代理模式的对象职责:

能够提供对象的替代品或其占位符。代理模式是将“原对象的控制权”委托给“代理”,让“代理”能够将请求提交给“原对象”前后进行一些处理。

在现实生活中,房东🙍‍♂️因为某些事情无法处理出售或出租房子🏠的时候,就会委托房产中介👨‍💼去帮他们进行出售或出租。这就是代理模式的一种真实世界类比。

❔为什么要使用代理模式,而不直接使用“原对象”呢?

👉 客户端有时无法直接操作某些对象。

📜 在分布式应用中,你需要调用的对象运行在另外一台服务器上。如果直接调用“原对象”,就需要处理网络服务,通过网络进行访问。我们可以使用代理模式,建立一个网络代理对象,就只需要调用代理对象就可以进行访问。

👉 客户端执行某些耗时操作容易造成服务端阻塞。

📜 在云编辑器里进行文案编写时,使用者不希望在执行拷贝图片的操作后,打字无法正常操作,甚至无法查看其他页面。我们可以使用代理模式,通过标示图片所在地址,用代理对象去读取图片资源。

👉 服务端需要控制客户端的访问权限。

📜 某一项业务由于安全原因只能让一部分特定的用户去访问。我们可以使用代理模式,让代理对象去接收所有请求,再有代理对象做权限判断。

💡 还有“某个接口可能需要外部额外操作”、“系统一直保存无用的、占用大资源的对象”等情况,也能考虑使用代理模式。


模式结构

  • 服务接口(Service Interface)声明了服务接口。代理必须遵循该接口才能伪装成服务对象。

  • 服务(Service)类提供了一些实用的业务逻辑。

  • 代理(Proxy)类包含一个指向服务对象的引用成员变量。代理完成其任务(例如延迟初始化、记录日志、访问控制和缓存等)后会将请求传递给服务对象。通常情况下, 代理会对其服务对象的整个生命周期进行管理。

  • 客户端(Client)能通过同一接口与服务或代理进行交互,所以你可在一切需要服务对象的代码中使用代理。

代理模式的类图:


模式实现

该示例使用代理模式将耗时的打印机实例初始化交由代理类负责。代理类让其只有在需要打印的时候才会开始实例化打印机。

示例程序的类图

代码实现

服务接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package example;

/** 打印机服务接口 */
public interface Printable {
/**
* 设置打印机名字
* @param name 打印机名字
*/
void setPrinterName(String name);

/**
* 获取打印机名字
* @return 打印机名字
*/
String getPrinterName();

/**
* 打印
* @param value 要打印的内容
*/
void print(String value);
}
服务
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
package example;

/** 打印机服务 */
public class Printer implements Printable {
/** 打印机名字 */
private String name;

/** 生成实例需要5秒 */
public Printer(String name) {
this.name = name;
System.out.println("正在生成Printer实例...");
heavyJob();
System.out.println("已生成Printer实例!");
}

/**
* 设置打印机名字
* @param name 打印机名字
*/
@Override
public void setPrinterName(String name) {
this.name = name;
}

/**
* 获取打印机名字
* @return 打印机名字
*/
@Override
public String getPrinterName() {
return this.name;
}

/**
* 打印
* @param value 要打印的内容
*/
@Override
public void print(String value) {
System.out.println("-----* " + this.name + " *-----");
System.out.println(value);
}

/** 模拟生成实例需要耗费大量时间 */
private void heavyJob() {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + 1 + "秒...");
}
}
}
代理
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
package example;

/** 打印机代理 */
public class PrinterProxy implements Printable {
/** 打印机名字 */
private String name;
/** 打印机服务对象 */
private Printer real;

public PrinterProxy(String name) {
this.name = name;
}

/**
* 设置打印机名字
* @param name 打印机名字
*/
@Override
public synchronized void setPrinterName(String name) {
this.name = name;
}

/**
* 获取打印机名字
* @return 打印机名字
*/
@Override
public String getPrinterName() {
return this.name;
}

/**
* 打印
* @param value 要打印的内容
*/
@Override
public void print(String value) {
realize();
real.print(value);
}

/** 生成实例 */
private synchronized void realize() {
if (real == null) {
real = new Printer(name);
}
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
import example.Printable;
import example.PrinterProxy;

/** 测试代理模式 */
public class Test {
public static void main(String[] args) {
Printable p = new PrinterProxy("Hellovie");
System.out.println("初始化打印机名字为:" + p.getPrinterName());
p.setPrinterName("The Hellovie");
System.out.println("修改打印机名字为:" + p.getPrinterName());
p.print("代理模式测试");
}
}

输出结果

1
2
3
4
5
6
7
8
9
初始化打印机名字为:Hellovie
修改打印机名字为:The Hellovie
正在生成Printer实例...
1秒...
2秒...
3秒...
已生成Printer实例!
-----* The Hellovie *-----
代理模式测试

生成 Printer 实例需要耗时 3 秒,使用 PrinterProxy 代理类让生成实例的时机放在调用 print 方法的时候。实现打印机只有在打印的时候才会生成实例。


常用场景和解决方案

  • Virtual Proxy(虚拟代理),适用于延迟初始化,用小对象表示大对象的场景。真正需要原对象实例时,才生成和初始化。
  • Access Proxy(保护代理),适用于服务端对客户端的访问控制场景。为原对象的功能做一些访问限制。
  • Remote Proxy(远程代理),适用于需要本地执行远程服务代码的场景。调用远程原对象如同在身边一样。
  • 日志记录代理,适用于需要保存请求对象历史记录的场景。在原对象周围添加日志监控。
  • 缓存代理,适用于缓存客户请求结果并对缓存生命周期进行管理的场景。对重复请求的相同结果进行缓存。
  • 智能引用,适用于在没有客户端使用某个重量级对象时立即销毁该对象。代理会将所有获取了指向服务对象或其结果的客户端记录在案。代理会时不时地遍历各个客户端,检查它们是否仍在运行。如果相应的客户端列表为空,代理就会销毁该服务对象,释放底层系统资源。代理还可以记录客户端是否修改了服务对象。其他客户端还可以复用未修改的对象。

模式的优缺点

优点 缺点
你可以在客户端毫无察觉的情况下控制服务对象。 代码可能会变得复杂,因为需要新建许多类。
如果客户端对服务对象的生命周期没有特殊要求,你可以对生命周期进行管理。 服务响应可能会延迟。
即使服务对象还未准备好或不存在,代理也可以正常工作。
开闭原则。你可以在不对服务或客户端做出修改的情况下创建新代理。

使用代理模式的优势

  • 作为接口的特定中间层能够降低对象间的直接耦合。
  • 虚拟代理通过延迟加载以及使用小对象代表大对象的方式,帮助减少系统资源的损耗,提升系统运行速度。
  • 保护代理可以控制客户端对服务端的访问权限。
  • 远程代理帮助客户端快速访问分布式机器上的对象,分布式服务器可以提供集群负载均衡、故障容错和高性能的计算能力。
  • 日志代理记录能记录每次操作的信息,对于用户使用轨迹跟踪、数据统计、定位问题等都有重要作用。
  • 缓存代理能够提供各式各样的缓存结果,对于需要高频读取重复数据的系统来说,能极大地提升系统性能。
  • 智能引用能够销毁长时间未被使用却占用大量资源的对象,释放底层资源。还能为原对象提供一些额外的操作被使用。

使用代理模式的劣势

  • 因为在客户端和服务器之间增加了代理对象,所以增加了系统的复杂度。
  • 实现代理模式的服务,如果处理请求时间过长,容易造成多个服务调用阻塞,从而影响整体系统的处理速度。
  • 对于一些偏操作系统或标准协议等底层的代理服务而言,代码实现可能很复杂。

拓展知识

  • 适配器模式能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰模式则能为对象提供加强的接口。
  • 装饰和代理两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制。
  • 代理模式的应用比装饰模式更广泛,代理模式不执着于链式结构,而是采用更为灵活的单一结构。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1