0%

设计模式之Proxy模式

只在必要时生成实例

介绍

Proxy是”代理人”的意思,它指的是代替别人进行工作的人,党不一定需要本人亲自进行工作时,就可以寻找代理人去完成工作.但代理人毕竟是代理人,能代替本人做的事请终究是有限的,因此,当代理人遇到无法自己解决的事请时就会去找本人解决该问题.

  • 例子
    • 提到代理,大家应该都会想到WEB代理,web代理位于web服务器和web客户端之间,为web页面提供高速缓存等功能.我们大致可以把他理解成代理模式.
    • 猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类.

      示例程序

      本人去处理所有事情是非常麻烦的,特别是初始化的时候都非常的耗时,因此使用代理,不到必须自己出马的时候一直按兵不动,让代理去完成这些工作,这就是代理模式.

  1. Printable接口:代理的同源性:
    1
    2
    3
    4
    5
    6
    package com.edu.tju.GOF.Proxy;
    public interface Printable {
    public abstract void setPrinterName(String name); // 设置名字
    public abstract String getPrinterName(); // 获取名字
    public abstract void print(String string); // 显示文字(打印输出)
    }
  2. Printer类:本人(相当于真正的服务器)
    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
    package com.edu.tju.GOF.Proxy;

    public class Printer implements Printable {
    private String name;
    public Printer() {
    heavyJob("正在生成Printer的实例");
    }
    public Printer(String name) { // 构造函数
    this.name = name;
    heavyJob("正在生成Printer的实例(" + name + ")");
    }
    public void setPrinterName(String name) { // 设置名字
    this.name = name;
    }
    public String getPrinterName() { // 获取名字
    return name;
    }
    public void print(String string) { // 显示带打印机名字的文字
    System.out.println("=== " + name + " ===");
    System.out.println(string);
    }
    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("结束.");
    }
    }
  3. ProxyPrinter代理类:(代理服务器)
    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
    package com.edu.tju.GOF.Proxy;


    public class PrinterProxy implements Printable {
    private String name; // 名字
    private Printer real; // “本人”
    public PrinterProxy() {
    }
    public PrinterProxy(String name) { // 构造函数
    this.name = name;
    }
    public synchronized void setPrinterName(String name) { // 设置名字
    if (real != null) {
    real.setPrinterName(name); // 同时设置“本人”的名字
    }
    this.name = name;
    }
    public String getPrinterName() { // 获取名字
    return name;
    }
    public void print(String string) { // 显示
    realize();
    real.print(string);
    }
    private synchronized void realize() { // 生成“本人”
    if (real == null) {
    real = new Printer(name);
    }
    }
    }
  4. Main(客户端)
    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
    package com.edu.tju.GOF.Proxy;


    public class PrinterProxy implements Printable {
    private String name; // 名字
    private Printer real; // “本人”
    public PrinterProxy() {
    }
    public PrinterProxy(String name) { // 构造函数
    this.name = name;
    }
    public synchronized void setPrinterName(String name) { // 设置名字
    if (real != null) {
    real.setPrinterName(name); // 同时设置“本人”的名字
    }
    this.name = name;
    }
    public String getPrinterName() { // 获取名字
    return name;
    }
    public void print(String string) { // 显示
    realize();
    real.print(string);
    }
    private synchronized void realize() { // 生成“本人”
    if (real == null) {
    real = new Printer(name);
    }
    }
    }
  • 时序图

可以看到服务器的启动实在是太耗时了(睡眠5秒钟来表示),那么使用代理服务器可以轻松地处理一些事务(设置名字,获得名字),直到代理服务器无能为力的时候(print打印内容),代理服务器就会通知服务器(本人)让服务器去处理

谁知道谁?

从本例可以看到,代理是清楚的知道被代理的对象的,因为使用了委托机制,将Printer对象组合进来),但是Printer是不知道代理的,它只是被启动了而已,这说明了什么?!本人(Printer)可以不做任何改动,就可以增加很多的代理去启动本人,这样非常利于可扩展性,其实这里也使用了懒加载模式,可以看到只有到不得不使用的时候才生成被代理的实例,那么可不可以直接在代理模式之中使用懒加载机制呢,答案是不利于可扩展性,没有这种分而治之的思想好,另外就是启动Printer类本身就是一种开销.同时我们看到了代理和被代理人都实现了同样的接口,这样的好处是很大的,在Main中可以随意切换,同时能够定义相同的必须的接口.这种透明性是非常有益的,在很多模式之中都有着体现.

角色

  • subject(主体,抽象类)
  • proxy(代理人,只有在自己不能处理的时候,才交给RealSubject去处理)
  • RealSubject(实际的主体、在代理人无法处理时出场)
  • client(调用者,可以随意调用proxy或realSubject)

中心思想:通过设置统一的接口(subject),使client可以不直接调用realSuject,而是通过调用proxy来实现业务功能.而在proxy里,相当于做了一层缓存,只有才必要时才调用realsubject

适用场景

Proxy模式有很多种变化形式

  • 延迟初始化 (虚拟代理).如果你有一个偶尔使用的重量级服务对象 ,一直保持该对象运行会消耗系统资源时,可使用代理模式.
    你无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候.本例子就是个虚拟代理

  • 访问控制 (保护代理). 如果你只希望特定客户端使用服务对象 ,这里的对象可以是操作系统中非常重要的部分 ,而客户端则是各种已启动的程序 (包括恶意程序) ,此时可使用代理模式.
    代理可仅在客户端凭据满足要求时将请求传递给服务对象.

  • 本地执行远程服务 (远程代理). 适用于服务对象位于远程服务器上的情形.

    • 在这种情形中 ,代理通过网络传递客户端请求 ,负责处理所有与网络相关的复杂细节.
    • 记录日志请求 (日志记录代理). 适用于当你需要保存对于服务对象的请求历史记录时. 代理可以在向服务传递请求前进行记录.
  • 缓存请求结果 (缓存代理). 适用于需要缓存客户请求结果并对缓存生命周期进行管理时 ,特别是当返回结果的体积非常大时.

    • 代理可对重复请求所需的相同结果进行缓存 ,还可使用请求参数作为索引缓存的键值.
    • 智能引用. 可在没有客户端使用某个重量级对象时立即销毁该对象.
    • 代理会将所有获取了指向服务对象或其结果的客户端记录在案. 代理会时不时地遍历各个客户端 ,检查它们是否仍在运行. 如果相应的客户端列表为空 ,代理就会销毁该服务对象 ,释放底层系统资源.
    • 代理还可以记录客户端是否修改了服务对象. 其他客户端还可以复用未修改的对象.

      优缺点:

  • 优点

    • 划分了proxy和realSubject两个角色,使得他们互相不影响,client可以选择单独使用realSubject或者使用代理来做,而不需要改变业务逻辑
    • 因为proxy和realSubject都实现了subject接口所以client可以不必在意,使用的是哪个具体类去实现逻辑.因此proxy可以说具有透明性,透过它就像可以直接看到realSubject一样
    • 远程代理使得客户端可以访问在远程机器上的对象,远程机器 可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求.
    • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系 统资源的消耗,对系统进行优化并提高运行速度.
    • 保护代理可以控制对真实对象的使用权限.
    • 你可以在客户端毫无察觉的情况下控制服务对象.
    • 开闭原则. 你可以在不对服务或客户端做出修改的情况下创建新代理.
  • 缺点

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

当然,我们可以看出,还是瑕不掩瑜的

相关的设计模式

  • 装饰和代理有着相似的结构 ,但是其意图却非常不同. 这两个模式的构建都基于组合原则 ,也就是说一个对象应该将部分工作委派给另一个对象. 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期 ,而装饰的生成则总是由客户端进行控制.
  • 适配器模式能为被封装对象提供不同的接口 ,代理模式能为对象提供相同的接口 ,装饰者模式则能为对象提供加强的接口.

反思

  1. 在示例程序中,PrinterProxy类知道Printer类.即在PrinterProxy类中显式地声明了Printer类的类名.怎样才能让其不知道Printer类呢?

我们可以利用反射 将生成实例的代码改为

1
real = (Printable)Class.forName(className).newInstance();

我们将修改PrinterProxy类和Main类

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
public class PrinterProxy implements Printable {
private String name; // 名字
private Printable real; // “本人”
private String className; // “本人”的类名
public PrinterProxy(String name, String className) { // 构造函数
this.name = name;
this.className = className;
}
public synchronized void setPrinterName(String name) { // 设置名字
if (real != null) {
real.setPrinterName(name); // 同时设置“本人”的名字
}
this.name = name;
}
public String getPrinterName() { // 获取名字
return name;
}
public void print(String string) { // 显示
realize();
real.print(string);
}
private synchronized void realize() { // 生成“本人”
if (real == null) {
try {
real = (Printable)Class.forName(className).newInstance();
real.setPrinterName(name);
} catch (ClassNotFoundException e) {
System.err.println("没有找到 " + className + " 类.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

Main类

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
Printable p = new PrinterProxy("Alice", "Printer");
System.out.println("现在的名字是" + p.getPrinterName() + ".");
p.setPrinterName("Bob");
System.out.println("现在的名字是" + p.getPrinterName() + ".");
p.print("Hello, world.");
}
}
  1. 在示例程序中,PrinterProxy类的setPrinterName方法和realize方法都是synchronized方法.如果不适用synchronized方法会有什么问题呢?

现成A在执行setPrinterName(“Bob”)的同时,现成B(通过print方法)调用了realize方法.这时,如果发生了线程切换,会出现PrinterProxy类的name字段的值为”Bob”,但Printer类的name确实Alice的问题
定义为synchronized方法,可以避免发生这样的线程切换,防止分别进行判断real字段值的处理和设置real字段值的处理.可以说synchronized方法”守护着”字段.

Thank you for your support