一、什么是Proxy模式
Proxy是“代理人”的意思,它指的是代替别人进行工作的人。当不一定需要本人亲自进行工作时,就可以寻找代理人去完成工作。但代理人毕竟只是代理人,能代替本人做的事情终究是有限的。因此,当代理人遇到无法自己解决的事情时就会去找本人解决该问题。
在面向对象编程中,“本人”和“代理人”都是对象。如果“本人”对象无法自己亲自完成一些工作,就将其交给“代理人”对象负责。
用一句话概况:只在必要时生成实例。
二、Proxy模式示例代码
这段示例程序实现了一个“带名字的打印机”。说是打印机,其实只是将文字显示在界面上而已。在Main类中会生成PrinterProxy类的实例(即“代理人”)。首先我们会给实例赋予名字Alice并在界面中显示该名字。接着会将实例名字改为Bob,然后显示该名字。在设置和获取名字时,都不会生成真正的Printer类的实例(即本人),而是由PrinterProxy类代理。最后,直到我们调用print方法,开始进入实际打印阶段后,PrinterProxy类才会生成Printer类的实例。
为了让PrinterProxy类与Printer类具有一致性,我们定义了Printable接口。示例程序的前提是“生成Printer类的实例”这一处理需要花费很多时间。为了在程序中体现这一点,我们在Printer类的构造函数中调用了heavyJob方法,让它干一些“重活”——睡眠5秒钟。
2.1 类之间的关系
类的功能表:
类图:
时序图:
2.2 Printer类
Printer类(代码清单21-1)是表示“本人”的类。
需要注意的是:heavyJob表示一个重活,也就是每秒只显示一个点,Printer类会因为构造函数里有该方法而不方便生成实例,所以我们在最后打印时再生成实例。也说明了前面讲的:只在必要时生成实例。
/**
* 打印机“本人”
*/
public class Printer implements Printable {
private String name;
public Printer() {
//构造函数里有“重活”,不方便打印,因此要将打印工作交给代理
heavyJob("正在生成Printer的实例");
}
public Printer(String name) {
this.name = name;
heavyJob("正在生成Printer的实例(" + name + ")");
}
/**
* 设置打印机的名字
*/
@Override
public void setPrinterName(String name) {
this.name = name;
}
/**
* 获取打印机的名字
*/
@Override
public String getPrinterName() {
return name;
}
@Override
public void print(String string) {
System.out.println("=== " + name + " ===");
System.out.println(string);
}
/**
* 一个重活,每秒只显示一个点,Printer类会因为构造函数里有该方法
* 而不方便打印,所以我们才将打印工作交给代理
*/
private void heavyJob(String msg) {
System.out.print(msg);
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.print(".");
}
System.out.println("结束。");
}
}
2.3 Printable接口
Printable接口用于使PrinterProxy类和Printer类具有一致性。
public interface Printable {
//设置打印机的名字
public abstract void setPrinterName(String name);
//获取打印机的名字
public abstract String getPrinterName();
//显示文字(打印输出)
public abstract void print(String string);
}
2.4 PrinterProxy类
PrinterProxy类是扮演“代理人”角色的类,它实现了Printable接口。
print方法已经超出了代理人的工作范围,因此它会调用realize方法来生成本人。Realize有“实现”(使成为真的东西)的意思。在调用realize方法后,real字段中会保存本人( Print类的实例),因此可以调用real.print方法。这就是“委托”。
不论setPrinterName方法和getPrinterName方法被调用多少次,都不会生成Printer类的实例。只有当真正需要本人时,才会生成Printer类的实例( PrinterProxy类的调用者完全不知道是否生成了本人,也不用在意是否生成了本人)。
这里希望大家记住的是,Printer类并不知道PrinterProxy类的存在。即,Printer类并不知道自己到底是通过PrinterProxy被调用的还是直接被调用的。
但反过来,PrinterProxy类是知道Printer类的。这是因为PrinterProxy类的real字段是Printer类型的。在PrinterProxy类的代码中,显式地写出了Printer这个类名。因此,PrinterProxy类是与Printer类紧密地关联在一起的组件,事实上他们也是可以解耦的。
相信细心的读者应该已经发现了Printer类的setPrinterName方法和realize方法都是synchronized方法。如果不使用synchronized,当多个线程几乎同时调用该方法时,在判断是否已经生成实例时可能会出错,导致new出多个实例。
public class PrinterProxy implements Printable{
//名字
private String name;
//“本人”
private Printer real;
public PrinterProxy() {}
public PrinterProxy(String name) {
this.name = name;
}
/**
* setPrinterName方法用于设置新的打印机名字。
* 如果real字段不为null(也就是已经生成了“本人”),那么会设置“本人”的名字",
同时设置自己( PrinterProxy的实例)的名字。
* 但是当real字段为null时(即还没有生成“本人”),那么只会设置自己( PrinterProxy的实例)的名字。
*/
@Override
public synchronized void setPrinterName(String name) {
if (real != null) {
real.setPrinterName(name);
}
this.name = name;
}
/**
* 返回自己的name字段
* @return 自己的name字段
*/
@Override
public String getPrinterName() {
return name;
}
/**
* print方法已经超出了代理人的工作范围,因此它会调用realize方法来生成本人
* @param string 打印机的名字
*/
@Override
public void print(String string) {
realize();
real.print(string);
}
/**
* 调用realize方法后,real字段中会保存本人(Print类的实例),
* 因此可以调用real.print方法。这就是“委托”
*/
private synchronized void realize() {
if (real == null) {
real = new Printer(name);
}
}
}
2.5 Main类
Main类通过PrinterProxy类使用Printer类。Main类首先会生成PrinterProxy,然后调用getPrinterName方法获取打印机名并显示它。之后通过setPrinterName方法重新设置打印机名。最后,调用print方法输出"Hello.world."。
请注意,在设置名字和显示名字之间并没有生成Printer的实例(本人),直至调用print方法后,Printer的实例才被生成。
public class Main {
public static void main(String[] args) {
Printable p = new PrinterProxy("Alice");
System.out.println("现在的名字是 " + p.getPrinterName() + "。");
p.setPrinterName("Bob");
System.out.println("现在的名字是 " + p.getPrinterName() + "。");
p.print("Hello, world.");
}
}
2.6 运行结果
三、拓展思路的要点
3.1 使用代理人来提升处理速度
在Proxy模式中,Proxy 角色作为代理人尽力肩负着工作使命。例如,在示例程序中,通过使用Proxy 角色,我们成功地将耗时处理(生成实例的处理)推迟至print方法被调用后才进行。
示例程序中的耗时处理的消耗时间并不算太长,大家可能感受不深。请大家试想一下,假如在一个大型系统的初始化过程中,存在大量的耗时处理。如果在启动系统时连那些暂时不会被使用的功能也初始化了,那么应用程序的启动时间将会非常漫长,这将会引发用户的不满。而如果我们只在需要使用某个功能时才将其初始化,则可以帮助我们改善用户体验。
3.2 有必要划分代理人和本人吗
当然,我们也可以不划分PrinterProxy类和Printer类,而是直接在Printer类中加入惰性求值功能(即只有必要时才生成实例的功能)。不过,通过划分PrinterProxy角色和Printer角色,可以使它们成为独立的组件,在进行修改时也不会互相之间产生影响(分而治之)。
只要改变了PrinterProxy类的实现方式,即可改变在Printable接口中定义的那些方法,即对于“哪些由代理人负责处理,哪些必须本人负责处理”进行更改。而且,不论怎么改变,都不必修改Printer类。如果不想使用惰性求值功能,只需要修改Main类,将它使用new关键字生成的实例从PrinterProxy类的实例变为Printer类的实例即可。由于PrinterProxy类和Printer类都实现了Printable接口,因此Main类可以放心地切换这两个类。
在示例程序中,PrinterProxy类代表了“Proxy角色”。因此使用或是不使用PrinterProxy类就代表了使用或是不使用代理功能。
3.3 代理与委托
代理人只代理他能解决的问题。当遇到他不能解决的问题时,还是会“转交”给本人去解决。这里的“转交”就是在本书中多次提到过的“委托”。从PrinterProxy类的print方法中调用real.print方法正是这种“委托”的体现。
在现实世界中,应当是本人将事情委托给代理人负责,而在设计模式中则是反过来的。
3.4 透明性
PrinterProxy类和Printer类都实现了Printable接口,因此Main类可以完全不必在意调用的究竟是PrinterProxy类还是Printer类。无论是直接使用Printer类还是通过PrinterProxy类间接地使用Printer类都可以。
在这种情况下,可以说PrinterProxy类是具有“透明性”的。就像在人和一幅画之间放置了一块透明的玻璃板后,我们依然可以透过它看到画一样,即使在Main类和Printer类之间加入一个PrinterProxy类,也不会有问题。
3.5 HTTP代理
提到代理,许多人应该都会想到HTTP代理。HTTP代理是指位于HTTP服务器(Web服务器)和HTTP客户端(Web浏览器)之间,为Web页面提供高速缓存等功能的软件。我们也可以认为它是一种 Proxy模式。
HTTP代理有很多功能。作为示例,我们只讨论一下它的页面高速缓存功能。
通过Web浏览器访问Web页面时,并不会每次都去访问远程Web服务器来获取页面的内容,而是会先去获取HTTP代理缓存的页面。只有当需要最新页面内容或是页面的缓存期限过期时,才去访问远程Web服务器。
在这种情况下,Web服务器扮演的是Client 角色,HTTP代理扮演的是Proxy角色,而Web服务器扮演的则是RealSubject角色。
3.6 各种Proxy模式
Proxy模式有很多种变化形式。
- Virtual Proxy(虚拟代理)
Virtual Proxy就是本章中学习的Proxy模式。只有当真正需要实例时,它才生成和初始化实例。
- Remote Proxy(远程代理)
Remote Proxy可以让我们完全不必在意RealSubject角色是否在远程网络上,可以如同它在自己身边一样(透明性地)调用它的方法。Java的RMI (RemoteMethodInvocation:远程方法调用)就相当于Remote Proxy。
- Access Proxy
Access Proxy用于在调用RealSubject角色的功能时设置访问限制。例如,这种代理可以只允许指定的用户调用方法,而当其他用户调用方法时则报错。
四、相关的设计模式
4.1 Adapter模式
Adapter模式适配了两种具有不同接口(API)的对象,以使它们可以一同工作。
而在Proxy模式中,Proxy角色与RealSubject角色的接口(API)是相同的(透明性)。
设计模式学习(三):Adapter适配器模式_玉面大蛟龙的博客-CSDN博客
4.2 Decorator模式
Decorator模式与Proxy模式在实现上很相似,不过它们的使用目的不同。
Decorator模式的目的在于增加新的功能。而在Proxy模式中,与增加新功能相比,它更注重通过设置代理人的方式来减轻本人的工作负担。