《设计模式》— 结构型模式 — 代理模式
一、动机
对一个对象进行访问控制的一个原因是只有我们确实需要这个对象时才对它进行创建和初始化。我们考虑一个可以在文档中嵌入图形对象的文档编辑器。有些图形对象的创建开销很大。但是打开文档必须很迅速,因此我们在打开文档时应避免一次性创建所有开销很大的对象。因为并非所有这些对象在文档中都同时可见,所以也没有必要同时创建这些对象。
这一限制条件意味着,对于每一个开销很大的对象,应该根据需要进行创建,当一个图像变为可见时会产生这样的需求。但是在文档中我们用什么来代替这个图像呢?又如何才能隐藏根据需要创建图像这一事实,从而不会是的编辑器的实现复杂化呢?例如,这种优化不应影响绘制和格式化的代码。
问题的解决方案是使用另一个对象(即图像代理)替代那个真正的图像。代理可以代替一个图像对象,并且在需要时负责实例化这个图像对象。
只有当文档编辑器激活图像代理的 draw 操作以及显示这个图像的时候,图像代理才创建真正的图像。代理直接将随后的请求转发给这个图像对象。因此在创建这个图像以后,它必须有一个指向这个图像的引用。
我们架设图乡村处在一个独立的文件中。这样我们可以把文件名作为实际对象的引用。代理还存储了图像的尺寸,即它的长和宽。有了图像尺寸,代理不需要真正实例化这个图像就可以响应格式化程序对图像尺寸的请求。
文档编辑器通过抽象的 Graphic 类定义的接口访问嵌入的对象。ImageProxy 是那些根据需要创建的图像代理类,其中保存了文件名作为指向磁盘上的图像文件的指针(语义上的)。该文件名被作为一个参数传递给 ImageProxy 的构造器。
ImageProxy 还存除了这个图想的边框以及对真正的 Image 实例的指引,直到代理实例化真正的图像时,这个指引才有效。Draw 操作必须保证在像这个图像转发请求之前,它已经被实例化了。GetExtent 只有在图像被实例化后才向它传递请求,否则,ImageProxy 返回它存储的图像尺寸。
二、适用性
在需要比较通用和复杂的对象指针代替简单的指针时,使用代理模式。下面是一些可以使用代理模式的常见情况:
- 远程代理为一个对象在不同的地址空间提供局部代表。从实现层次来说,我们使用的服务器代理和反向代理就属于远程代理。
- 虚代理根据需要创建开销很大的对象。在读写文件时,由于系统IO较为耗时,我们经常会使用缓存技术读取一整块区域到缓存中,而不是用户需要多少就加载多少。这种技术属于虚代理。
- 保护代理控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。我们在开发中使用的钩子、过滤器等函数就是这种技术的延伸。
- 智能指引取代了简单的指针,它在访问对象时执行一些附加操作。它的典型用途包括引用计数、自动加锁解锁等。
三、结构
四、参与者
1、Proxy
- 保存一个引用使得代理可以访问实体。若 RealObject 和 Subject 的接口相同,Proxy 会引用 Subject。
- 提供一个与 Subject 的接口相同的接口,这样代理就可以用来替代实体。
- 控制对实体的存取,并可能负责创建和删除它。
其他功能依赖于代理的类型:
- 远程代理负责队请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。
- 虚代理可以缓冲实体的附加信息,以便延迟对它的访问。
- 保护代理检查调用者是否具有事先一个请求所必须的访问权限。
2、Subject
定义 RealSubject 和 Proxy 的共用接口,这样就在任何使用 realSubject 的地方都可以使用 Proxy。
3、RealSubject
定义 Proxy 所代理的实体。
五、实现
代理模式可以利用以下一些语言特性
1、C++中的存取运算符
C++支持重载运算符->。重载这一运算符使你可以在解引用一个对象时,执行一些附加的操作。这一点可以用于实现某些种类的代理,代理的作用就像一个指针。智能指针都实现了该操作符用于提供将操作转发至原始指针。
2、代理并不总是需要知道实体的类型
若代理类能够完全通过一个抽象接口处理他的实体,则无需为每一个 RealSubject 类都生成一个代理类,代理可以统一处理所有的 RealSubject 类。但是如果代理需要实例化 RealSubject,那么它们必须知道具体的类。
另一个实现方面的问题设计在实例化实体以前怎样保存它的引用。我们可以通过文件名等对象标识符来记录 realObject。
六、实现
我们在 java 中可以借助反射方便地实现动态代理。
1、抽象类及被代理类
interface Graphic {
void draw();
}
class Image implements Graphic{
@Override
public void draw() {
System.out.println("Image draw");
}
}
2、代理工厂
为了方便起见,我们这里写死了处理所有类对象的 handler。实际中需要动态地配置映射关系:
class ProxyFactory {
public static Object getProxyInstance(Object obj) {
// 根据配置文件或其它方式动态配置类及代理类handler的映射关系
MyInvocationHandler myInvocationHandler = new FilterInvocationHandler();
myInvocationHandler.bind(obj);
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), myInvocationHandler);
}
}
3、handler
我们提供了 beforeInvoke 和 afterInvoke 使得子类通过重写这两个方法便可以修改被代理类接口调用前后的过滤功能。这是一种简单的 AOP 实现方式。
class MyInvocationHandler implements InvocationHandler {
private Object obj;
final public void bind(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
beforeInvoke();
Object retValue = method.invoke(obj, args);
afterInvoke();
return retValue;
}
protected void beforeInvoke() {}
protected void afterInvoke() {}
}
class FilterInvocationHandler extends MyInvocationHandler {
protected void beforeInvoke() {
System.out.println("FilterInvocationHandler beforeInvoke");
}
protected void afterInvoke() {
System.out.println("FilterInvocationHandler afterInvoke");
}
}
4、测试
public class ProxyTest {
@Test
void testProxy() {
Image image = new Image();
Graphic graphic = (Graphic)ProxyFactory.getProxyInstance(image);
graphic.draw();
}
}
七、相似设计模式
代理模式和修饰模式很像。不过二者在设计上的侧重点不同。修饰模式强调为对象添加功能,而代理模式强调对对象的访问控制和处理。