Design Pattern: Proxy 模式
学习是分享和合作式的!
转载请注明出处:https://blog.csdn.net/wdzxl198/article/details/10472999;
文章摘自: https://www.riabook.cn/doc/designpattern/;
在 Gof 的书中对Proxy模式的目的给定为:为其它的物件提供一种代理,以控制对这个物件的访问。由这句话所延伸出来的意思是,根据您的目的不同,您的代理物件将负有不同的责任,因为产生多种不同的代理情况。根据不同的代理目的,而有不同的代理情况,在Gof中所举的一个例子是Virtual Proxy,当中举一个文档中内嵌图片的例子,假设您的图片是在文档分页的后面,一开始您并不用直接载入图片,而使用一个虚代理物件,代替图片被载入,以 求开启一个文档的时候,速度能够加快。当您卷动文档至该显示图片的页数时,这时再载入图片。

如上图所示,当文档被开启时,ImageProxy物件代理Image物件被载入,在还没卷动至图片显示处时,也就是还没有调用 ImageProxy的draw()时,图片并不会被载入,因而可以加速文档的开启与节省记忆体的使用;如果需要显示图片了,ImageProxy的 draw()会被调用,而这时才真正创建Image物件,以从硬碟中载入图片。
Proxy模式的 UML 结构图如下所示:

在调用RealSubject的request()之前,Proxy物件也许会有一些预先处理的操作,就假设我们组织为preOperation()与 postOperation()好了,当客户对Proxy发出request()请求后,一个可能的时序图如下所示:

您的preOperation()与postOperation()正决定了Proxy模式使用于何种情况,例如一个Remote Proxy的情况,可以为一个远端真实物件提供一个局部代表;Protection Proxy控制对物件的访问,您可以使用它来作不同级别、权限的存取控制;Cache Proxy为一个物件提供临时的储存,使得许多客户端都能直接存取它,而不用对真实物件直接要求,只有在必要的时候更新这个临时物件,或是让客户直接存取 真实物件。
现在来看看实现代理的两种方式:Static Proxy与Dynamic Proxy。严格来说这是属于模式的实现方式,不过藉由实例可以更了解Proxy模式的应用。
先来看个例子,这个例子是记录(log)动作,程式中很常需要为某些动作或事件作下记录,以便在事后检视或是作为除错时的资讯,一个最简单的例子如下:
- HelloSpeaker.java
import java.util.logging.*; public class HelloSpeaker { private Logger logger = Logger.getLogger(this.getClass().getName()); public void hello(String name) { logger.log(Level.INFO, "hello method starts...."); System.out.println("Hello, " + name); logger.log(Level.INFO, "hello method ends...."); } }
HelloSpeaker在执行hello()方法时,您希望能记录该方法已经执行及结束,最简单的作法就是如上在执行的前后加上记录动作,然而 Logger介入了HelloSpeaker中,记录这个动作并不属于HelloSpeaker,这使得HelloSpeaker增加了非业务上需要的逻 辑在当中。
想想如果程式中这种记录的动作到处都有需求,上面这种写法势必造成必须复制记录动作的程式码,使得维护记录动作的困难度加大。如果不只有记录动作,有一些 非物件本身职责的相关动作也混入了物件之中(例如权限检查、事务管理等等),会使得物件的负担更形加重,甚至混淆了物件的职责,物件本身的职责所占的程式 码,或许远小于这些与物件职责不相关动作的程式码。
怎么办,用下面的方法或许好一些,先定义一个介面,然后实作该介面:
- IHello.java
public interface IHello { public void hello(String name); }
- HelloSpeaker.java
public class HelloSpeaker implements IHello { public void hello(String name) { System.out.println("Hello, " + name); } }
接下来实作一个代理物件HelloProxy:
- HelloProxy.java
import java.util.logging.*; public class HelloProxy implements IHello { private Logger logger = Logger.getLogger(this.getClass().getName()); private IHello helloObject; public HelloProxy(IHello helloObject) { this.helloObject = helloObject; } public void hello(String name) { logger.log(Level.INFO, "hello method starts...."); helloObject.hello(name); logger.log(Level.INFO, "hello method ends...."); } }
执行时可以如此:
IHello helloProxy = new HelloProxy(new HelloSpeaker());
helloProxy.hello("Justin");
helloProxy.hello("Justin");
代理物件HelloProxy将代理真正的HelloSpeaker来执行hello(),并在其前后加上记录的动作,这使得 HelloSpeaker在撰写时不必介入记录动作,HelloSpeaker可以专心于它的职责。
这是Static Proxy的基本范例,然而如您所看到的,代理物件的一个介面只服务于一种类型的物件,而且如果要代理的方法很多,势必要为每个方法进行代理, Static Proxy在程式规模稍大时就必定无法胜任。
Java在JDK 1.3之后加入协助开发Dynamic Proxy功能的类别,我们不必为特定物件与方法撰写特定的代理,使用Dynamic Proxy,可以使得一个handler服务于各个物件,首先,一个handler必须实现 java.lang.reflect.InvocationHandler:
- LogHandler.java
import java.util.logging.*; import java.lang.reflect.*; public class LogHandler implements InvocationHandler { private Logger logger = Logger.getLogger(this.getClass().getName()); private Object delegate; public Object bind(Object delegate) { this.delegate = delegate; return Proxy.newProxyInstance( delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { logger.log(Level.INFO, "method starts..." + method); result = method.invoke(delegate, args); logger.log(Level.INFO, "method ends..." + method); } catch (Exception e){ logger.log(Level.INFO, e.toString()); } return result; } }
InvocationHandler的invoke()方法会传入被代理物件的方法名称与执行参数实际上要执行的方法交由method.invoke (),并在其前后加上记录动作,method.invoke()传回的物件是实际方法执行过后的回传结果。
Dynamic Proxy必须宣告介面,实作该介面,例如:
- IHello.java
public interface IHello { public void hello(String name); }
- HelloSpeaker.java
public class HelloSpeaker implements IHello { public void hello(String name) { System.out.println("Hello, " + name); } }
java.lang.reflect.Proxy的newProxyInstance()依要代理的物件、介面与handler产生一个代理物件,我们可 以使用下面的方法来执行程式:
LogHandler logHandler = new LogHandler();
IHello helloProxy = (IHello) logHandler.bind(
new HelloSpeaker());
helloProxy.hello("Justin");
IHello helloProxy = (IHello) logHandler.bind(
new HelloSpeaker());
helloProxy.hello("Justin");
LogHandler不在服务于特定物件与介面,而HelloSpeaker也不用插入任何有关于记录的动作,它不用意识到记录动作的存在。
最后更新:2017-04-03 16:49:02