设计模式——11、代理模式
  • 作者:ZJWave
  • 分类: 设计模式
  • 发表:2019-07-01 20:45
  • 围观:5934
  • 评论:0

玩过扮白脸、扮黑脸的游戏吗?你是一个白脸,提供很好且很友善的服务,但是你不希望每个人都叫你做事,所以找了黑脸控制对你的访问。这就是代理要做的:控制和管理访问。就像你将看到的,代理的方式有许多种。代理以通过Internet为它们的代理对象搬运的整个方法调用而出名,它也可以代替某些懒惰的对象做一些事情。

1.为糖果机增加监视器

万能糖果公司CEO:还记得我吧!我就是万能糖果公司的CEO。各位组员,我真的希望我的糖果机能够获得更好的监控,你能找到方法给我一份库存以及机器状态的报告吗?

听起来很容易,如果你还记得我们已经得到了可以取得糖果数量的getCount()方法和取得糖果机状态的getState()方法。

我们所需要做的事,就是创建一份能打印出来的报告,然后把它递送给CEO。这个嘛!我们可能需要为每个糖果机加上一个位置的字段,这样CEO就可以一目了然。

让我们现在就开始编码。这一定会让CEO印象深刻,让他对我们彻底改观。

为监视器编码

我们先为GumballMachine加上处理位置的支持:

public class GumballMchine {
    // 其他的实例变量
    String location;
    
    public GumballMchine(String location,int count) {
        this.location = location;
    }

    public String getLocation() {
        return location;
    }
    
    //其他方法
}

现在让我们创建另一个类,GumballMonitor(糖果监视器),以便取得机器的位置、糖果的库存量以及当前机器的状态,并打印成一份报告。

public class GumballMonitor {

    GumballMachine machine;

    /**
     * 此监视器的构造器需要被传入糖果机,
     * 它会将糖果机记录在machine实例变量中。
     * @param machine
     */
    public GumballMonitor(GumballMachine machine) {
        this.machine = machine;
    }

    /**
     * 负责打印报告的report方法,会将位置、库存、机器状态打印出来。
     */
    public void report(){
        System.out.println("Gumball Machine: " + machine.getLocation());
        System.out.println("Current Inventory: " + machine.getCount() + " gumballs");
        System.out.println("Current State: " + machine.getState());
    }
}

测试监视器

我们一下就搞定了,CEO将对我们的开发能力感到折服。

现在我们需要实例化一个GumballMonitor(糖果监视器),并传入一个糖果机:

public class GumballMachineTestDrive {

    public static void main(String[] args) {
        int count = 0;
        if(args.length < 2){
            //利用命令行传入位置和一开始的糖果数目
            System.out.println("GumballMachine <name> <inventory>");
            System.exit(1);
        }

        count = Integer.parseInt(args[1]);

        GumballMachine gumballMachine = new GumballMachine(args[0], count);

        GumballMonitor monitor = new GumballMonitor(gumballMachine);

        //其他的测试代码

        //当我们需要机器的报告的时候,调用report()方法即可
        monitor.report();
    }

}

万能糖果公司CEO:监视器的输出看起来虽然很不错,但可能是我之前说的不够清楚,我需要的是在远程监控糖果机!事实上,我们已经把网络准备好了。

Joe:这让我们学到了一个教训:在开始编码之前,要先收集需求。希望我们不要再从头开始……

Frank:别担心!我已经会许多设计模式了,我们其实只是需要远程代理(remote proxy)罢了!

Joe:你说远程什么?

Frank:远程代理。你想想:我们已经写好监视器代码,对吧?我们给GumballMonitor一个糖果机的引用,它给我们一份报告。问题在于监视器和糖果机在同一个JVM上面执行,但是CEO希望在他的桌面上远程监控这些机器!所以我们可以不要变化GumballMonitor,不要将糖果机交给GumballMonitor,而是将一个远程对象的代理交给它。

Joe:我不太懂。

Jim:我也不懂。

Frank:让我从头开始说……所谓的代理(proxy),就是代表某个真实的对象。在这个案例中,代理就像是糖果机对象一样,但其实幕后是它利用网络和一个远程的真正糖果机沟通。

Jim:你是说,不需要改我们的代码,只要将GumballMachine代理版本的引用交给监视器就可以了……

Joe:然后这个代理假装它是真正的对象,但是其实一切的动作是它利用网络和真正的对象沟通。

Frank:差不多就是这样。

Joe:这好像说的比做的容易。

Frank:或许吧!但是我不认为有这么难。我们必须确定糖果机能够通过网络接受请求并且提供服务;我们也需要让监视器有办法取得代理对象的引用,这方面,幸好Java已经有一些很棒的内置工具可以帮助我们。我们先看看远程代理……

2.远程代理

远程代理就好比“远程对象的本地代表”。何谓“远程对象”?这是一种对象,活在不同的Java虚拟机(JVM)堆中(更一般的说法为,在不同的地址空间运行的远程对象)。何谓“本地代表”?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。

你的客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的低层细节。

Joe:真是个绝妙的注意。我们要写一些代码调用本地方法,然后传送到网络上,调用远程对象的一些方法。我猜想,当调用完毕,结果值也通过网络从远程送回我们的客户。我觉得这样的代码可能不好写。

Frank:等等,我们可没有要自己写这些代码,Java已经内置远程调用的功能了,我们只需要修改一下代码,让它符合RMI的要求就行了。

将远程代理加到糖果机的监视代码中年

构想上,这一切都很不错,但是要如何创建一个代理,知道如何调用在另一个JVM中年的对象的方法?

这个嘛!你不能取得另一个堆的对象的引用,换句话说,你不可以这么写:

Duck d = <另一个堆的对象>

变量d只能引用当前代码语句的同一堆空间的对象。那该怎么办?该是Java远程方法调用出现的时刻了……RMI可以让我们找到远程JVM内的对象,并允许我们调用它们的方法。

我们打算这么做:

  1. 首先,我们先浏览并了解一下RMI。即使你熟悉RMI,你可能还想复习顺便跟着浏览一下风景。
  2. 接着,我们会把GumballMachine变成远程服务,提供一些可以被远程调用的方法。
  3. 然后,我们将创建一个能和远程的GumballMachine沟通的代理,这需要用到RMI。最后再结合监视系统,CEO就可以监视任何数量的远程糖果机了。

远程方法101

加入我们想要设计一个系统,能够调用本地对象,然后将每个请求转发到远程对象上进行。要如何设计?我们需要一些辅助对象,帮我们真正进行沟通。这些辅助对象使客户就像在调用本地对象的方法(事实也是如此)一样。客户调用客户辅助对象上的方法,仿佛客户辅助对象即使真正的服务。客户辅助对象再负责为我们转发这些请求。

换句话说,客户对象以为它调用的是远程服务上的方法,因为客户辅助对象乔装成服务对象,假装自己有客户所要调用的方法。

但是客户辅助对象不是真正的远程服务。虽然操作看起来很像(因为具有服务所宣称的相同的方法),但是并不真正拥有客户所期望的方法逻辑。客户辅助对象会联系服务器,传送方法调用信息(例如,方法名称、变量等),然后等待服务器的返回。

在服务端,服务辅助对象从客户辅助对象中接收请求(透过Socket连接),将调用的信息解包,然后调用真正服务对象上的真正发方法。所以,对于服务对象来说,调用是本地的,来自服务辅助对象,而不是远程客户。

服务辅助对象从服务中得到返回值,将它打包,然后运回到客户辅助对象(通过网络Socket的输出流),客户辅助对象对信息解包,最后将返回值交给客户对象。

方法调用是如何发生的

客户对象调用客户辅助对象的doBigThing()方法。

客户辅助对象打包调用信息(变量、方法名称等),然后通过网络将它运给服务辅助对象。

③ 服务辅助对象把来自客户辅助对象的信息解包,找出被调用的方法(以及在哪个对象内),然后调用真正的服务对象上的真正方法。

 服务对象上的方法被调用,将结果返回给服务辅助对象。

 服务辅助对象把调用的返回信息打包,然后通过网络运回给客户辅助对象。

 客户辅助对象把返回值解包,返回给客户对象。对于客户来说,这是完全透明的。

2.1 Java RMI概观

现在你已经知道远程方法如果工作的要点,你还需要了解如何利用RMI进行远程方法调用。

RMI提供了客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI的好处在于你不必亲自写任何网络或I/O代码。客户程序调用远程方法(即真正的服务所在)就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。

RMI也提供了所有运行时的基础设施,好让这一切正常工作。这包括了查找服务(lookup service),这个服务用来寻找和访问远程对象。

关于RMI调用和本地(正常的)方法调用,有一个不通电。虽然调用远程方法就如同调用本地方法一样,但是客户辅助会通过网络发送方法调用,所以网络和I/O的确是存在的。关于网络和I/O部分,我们知道些什么?

我们知道网络和I/O是有风险的,容易失败的,所以随时都可能抛出异常,也因此,客户必须意识到风险的存在。

RMI将客户辅助对象称为stub(桩),服务辅助对象称为skeleton(骨架)。

现在,我们就来看看如何将对象变成服务——可以接受远程调用的服务。也看看,如何让客户做远程调用。

2.2 制作远程服务

这里有用来制作远程服务的五个步骤的概要。换句话说,这些步骤将一个普通的对象编程可以被远程客户调用的远程对象。我们稍后会把这些步骤应用于GumballMachine。现在,就让我们看看这些步骤的细节。

步骤一:制作远程接口

远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub和实际的服务都实现此接口。

步骤二:制作远程的实现

这是做实际工作的类,为远程接口中定义的远程方法提供了真正的实现。这就是客户真正想要调用方法的对象(例如,我们的GumballMachine)。

步骤三:利用rmic产生的stub和skeleton

这就是客户和服务的辅助类。你不需要自己创建这些类,甚至连生成它们的代码都不用看,因为当你运行rmic工具时,这都会自动处理。你可以在JDK中找到rmic。

步骤四:启动RMI registry(rmiregistry)

rmiregistry就像是电话簿,客户可以从中查到代理的位置(也就是客户的stub helper对象)。

步骤五:开始远程服务

你必须让服务对象开始运行。你的服务实现类会去实例化一个服务的示例,并将这个服务注册到RMI registry。注册之后,这个服务就可以供客户调用了。

2.2.1 制作远程接口

① 扩展java.rmi.Remote。

Remote是一个“记号”接口,所以Remote不具有方法。对于RMI来说,Remote接口具有特别的意义,所以我们必须遵守规则。请注意,我们这里说的是“扩展”(extends),因为接口可以“扩展”另一个接口。

//这表示此接口要用来支持远程调用
public interface MyRemote extends Remote{}

② 声明所有的方法都会抛出RemoteException。

客户使用远程接口调用服务。换句话说,客户会调用实现远程接口的Stub上的方法,而Stub底层用到了网络和I/O,所以各种坏事情都可以会发生。客户必须认识到风险,通过处理或声明远程异常来解决。如果接口中的方法声明了异常,任何在接口类型的引用上调用方法的代码也必须处理或声明异常。

import java.rmi.*;

public interface MyRemote extends Remote {

    /**
     * 每次远程方法调用都必须考虑成是“有风险的”。
     * 在每个方法中声明RemoteException,可以让客户注意到这件事,
     * 并了解这可能是无法工作的。
     */
    public String sayHello() throws RemoteException;

}

③ 确定变量和返回值是属于原语(primitive)类型或者可序列化(Serializable)类型。

远程方法的变量和返回值,必须属于原语类型或Serializable类型。这不难理解。远程方法的变量必须被打包并通过网络运送,这要靠序列化来完成。如果你使用原语类型、字符串和许多API中内定的类型(包括数组和集合),都不会有问题。如果你传送自己定义的类,就必须保证你的类实现了Serializable。

//这个返回值(String)将从服务器经过网络运回给客户,所以必须是Serializable的。
//这样,才可将变量和返回值打包并传送。
public String sayHello() throws RemoteException;

2.2.2 制作远程实现

① 实现远程接口

你的服务必须实现远程接口,也就是客户将要调用的方法的接口。

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    protected MyRemoteImpl() throws RemoteException {
    }

    @Override
    public String sayHello() throws RemoteException {
        return "Server says, 'Hey'";
    }
}

② 扩展UnicastRemoteObject

为了要成员远程服务对象,你的对象需要某些“远程的”功能。最简单的方式是扩展java.rmi.server.UnicastRemoteObject,让超类帮你做这些工作。

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {

③ 设计一个不带变量的构造器,并声明RemoteException。

你的新超类UnicastRemoteObject带来一个小问题:它的构造器抛出RemoteException。唯一解决这个问题的方法就是为你的远程实现声明一个构造器,这样就有了一个声明RemoteException的地方。当类被实例化的时候,超类的构造器总是会被调用。如果超类的构造器抛出异常,那么你只能声明子类的构造器也抛出异常。

protected MyRemoteImpl() throws RemoteException {}

④ 用RMI Registry注册此服务。

现在你已经有一个远程服务了,必须让他可以被远程客户调用。你要做的是将此服务实例化,然后让进RMI registry中(记得先确定RMI Registry正在运行,否则注册会失败)。当注册这个实现对象时,RMI系统其实注册的是stub,因为这是客户真正需要的。注册服务使用了java.rmi.Naming类的静态rebind()方法。

try {
    //为你的服务命名,好让客户用来在注册表中寻找它,
    //并在RMI registry中注册此名字和此服务。
    //当你绑定(bind)服务对象时,RMI会把服务器换成stub,
    //然后把stub放到registry中
    MyRemoteImpl service = new MyRemoteImpl();
    Naming.rebind("RemoteHello",service);
}catch (Exception ex){...}

2.2.3 产生Stub和Skeleton

在远程实现类(不是远程接口)上执行rmic

rmic是JDK内的一个工具,用来为一个服务类产生stub和skeleton。命名习惯是在远程实现的名字后面加上_Stub或_Skel。rmic有一些选项可以调整,包括不要产生skeleton、查看源代码,甚至使用IIOP作为协议。我们这里使用rmic的方式是常用的方式,将类产生在当前目录下(就是你cd到的地方)。请注意,rmic必须看到你的实现类,所以你可能会从你的远程实现所在的目录执行rmic(为了简单起见,我们这里不用package,但是在真实世界中,你必须注意package的目录结构和名称问题)。

//请注意,不需要在末尾加“.class”,只要类名称就可以了。
rmic MyRemoteImpl

由于rmic已经过时,因此这里就不再对远程代理做过多的介绍了。

3.定义代理模式

请注意,远程代理是一般代理模式的一种实现,其实这个模式的变体相当多。

现在,我们就来看看代理模式的定义:

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。

使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。

我们已经看到代理模式是如何为另一个对象提供替身的。我们也将代理描述成另一个对象的“代表”。

但是代理控制访问怎么解释呢?这听起来有一点奇怪。别担心,在糖果机的例子中年,代理控制了对远程对象的访问。代理之所以需要控制访问,是因为我们的客户(监视器)不知道如何和远程对象沟通。从某个方面来看,远程代理控制访问,好帮我们处理网络上的细节。正如同刚刚说过的,代理模式有许多变体,而这些变体几乎都和“控制访问”的做法有关。我们看看几种代理控制访问的方式:

  • 就像我们已经知道的,远程代理控制访问远程对象。
  • 虚拟代理控制访问创建开销大的资源。
  • 保护代理基于权限控制对资源的访问。

现在你已经有基本的改变了,来看看类图……

让我们详细看这张图……

首先是Subject,它为RealSubject和Proxy提供了接口。通过实现同一接口,Proxy在RealSubject出现的地方取代它。

RealSubject是真正做事的对象,它是被Proxy代理和控制访问的对象。

Proxy持有RealSubject的引用。在某些例子中,Proxy还会负责RealSubject对象的创建与销毁。客户和RealSubject的交互都必须通过Proxy。因为Proxy和RealSubject实现相同的接口(Subject),所以任何用到RealSubject的地方,都可以用Proxy取代。Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的控制。这些情况包括RealSubject是远程的对象、RealSubject创建开销大,或RealSubject需要被保护。

你已经了解了一半的代理模式,现在让我们看看,除了远程代理之外,代理模式还有哪些用法……

准备虚拟代理(Virtual Proxy)

你已经看过代理模式的定义,也看过一个特定的例子(远程代理),现在就让我们看看另一种代理:虚拟代理。你将发现,代理模式可以以很多形式显现,但都大致符合一般代理的设计。为何有这么多的形式呢?因为代理模式可以被用在许多不同的例子中。让我们现在看看虚拟代理和远程代理的比较:

① 远程代理

远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。

② 虚拟代理

虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

显示CD封面

我们打算建立一个应用程序,用来展现你最喜欢的CD封面。你可以建立一个CD标题菜单,然后从Amazon.com等网站的在线服务中年取得CD封面的图。如果你使用Swing,可以创建一个Icon接口从网络上加载图像。唯一的问题是,限于连接带宽和网络负载,下载可能需要一些时间,所以在等待图像加载的时候,应该显示一些东西。我们也不希望在等待图像时整个应用程序被挂起。一旦图像被加载完成,刚才显示的东西应该消失,图像显示出来。

想做到这样,简单的方式就是利用虚拟代理。虚拟代理可以代理Icon,管理背景的加载,并在加载未完成时显示“CD封面加载中,请稍后……”,一旦加载完成,代理就把显示的职责委托给Icon。

设计CD封面虚拟代理

在开始写CD封面浏览器代码之前,让我们看一下类图。此类图和远程代理的图很类似,但是这里的代理是用于隐藏创建开销大的对象(因为我们需要通过网络取得图像数据),而不是隐藏在网络其他地方的对象。

ImageProxy如何工作:

  1. ImageProxy首先创建一个ImageIcon,然后开始从网络URL上加载图像。
  2. 在加载的过程中,ImageProxy显示“CD封面加载中,请稍后……”
  3. 当图像加载完毕,ImageProxy把所有方法调用委托给真正的ImageIcon,这些方法包括了paintIcon()、getWidth()和getHeight()
  4. 如果用户请求新的图像,我们就创建新的代理,重复这样的过程。

编写ImageProxy

public class ImageProxy implements Icon {
    //此imageIcon是我们希望在加载后显示出来的真正的图像
    ImageIcon imageIcon;
    URL imageUrl;
    Thread retrievalThread;
    boolean retrieving = false;

    /**
     * 我们将图像的URL传入构造器中。
     * 这是我们希望显示的图像所在的位置。
     * @param imageUrl
     */
    public ImageProxy(URL imageUrl) {
        this.imageUrl = imageUrl;
    }

    @Override
    public void paintIcon(final Component c, Graphics g, int x, int y) {
        if(imageIcon != null){
            imageIcon.paintIcon(c,g,x,y);
        }else {
            g.drawString("Loading CD cover, please wait...", x + 300,x + 190);
            if(!retrieving){
                retrieving = true;
                retrievalThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //这里的代码会在屏幕上画出一个icon图像(通过委托给imageIcon)
                            //然而,如果我们没有被完整创建的ImageIcon,那就自己创建一个。
                            imageIcon = new ImageIcon(imageUrl,"CD Cover");
                            c.repaint();
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                });
                retrievalThread.start();
            }
        }
    }

    /**
     * 在图像加载完毕前,返回默认的宽和高。
     * 图像加载完毕后,转给imageIcon处理
     * @return
     */
    @Override
    public int getIconWidth() {
        if(imageIcon != null){
            return imageIcon.getIconWidth();
        }else {
            return 800;
        }
    }

    @Override
    public int getIconHeight() {
        if(imageIcon != null){
            return imageIcon.getIconHeight();
        }else {
            return 600;
        }
    }
}

测试CD封面浏览器

现在我们就来试试这个虚拟代理。我们已经写好了一个新的ImageProxyTestDrive。用来设置窗口、创建框架、安装菜单和创建我们的代理。我们不在这里研究这些代码的细节,部分测试代码在下面:

public class ImageProxyTestDrive {

    ImageComponent imageComponent;
    JFrame frame = new JFrame("CD Cover Viewer");
    JMenuBar menuBar;
    JMenu menu;
    Map<String,String> cds = new HashMap<>();

    public static void main(String[] args) throws Exception{
        new ImageProxyTestDrive();
    }

    public ImageProxyTestDrive() throws MalformedURLException {
        cds.put("Ambient: Music for Airports","http://images.amazon.com/images/P/B000003S2K.01.LZZZZZZZ.jpg");
        cds.put("Buddha Bar","http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
        cds.put("Ima","http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZ.jpg");
        cds.put("Karma","http://images.amazon.com/images/P/B000005DCB.01.LZZZZZZZ.gif");
        cds.put("MCMXC A.D","http://images.amazon.com/images/P/B000002URV.01.LZZZZZZZ.jpg");
        cds.put("Northern Exposure","http://images.amazon.com/images/P/B000003SFN.01.LZZZZZZZ.jpg");
        cds.put("Selected Ambient Works, Vol. 2","http://images.amazon.com/images/P/B000002MNZ.01.LZZZZZZZ.jpg");
        cds.put("oliver","http://www.cs.yale.edu/homes/freeman-elisabeth/2004/9/Oliver_sm.jpg");

        URL initialURL = new URL(cds.get("Selected Ambient Works, Vol. 2"));
        menuBar = new JMenuBar();
        menu = new JMenu("Favorite CDs");
        menuBar.add(menu);
        frame.setJMenuBar(menuBar);

        for (String name : cds.keySet()) {
            JMenuItem menuItem = new JMenuItem(name);
            menu.add(menuItem);
            menuItem.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    imageComponent.setIcon(new ImageProxy(getCDUrl(e.getActionCommand())));
                    frame.repaint();
                }
            });
        }

        //建立框架和菜单
        ImageProxy icon = new ImageProxy(initialURL);
        imageComponent = new ImageComponent(icon);
        frame.getContentPane().add(imageComponent);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);;
        frame.setSize(800,600);
        frame.setVisible(true);
    }

    private URL getCDUrl(String name) {
        try {
            return new URL(cds.get(name));
        } catch (MalformedURLException e) {
            e.printStackTrace();
            return null;
        }
    }
}

要测试的事情……

  1. 用菜单加载不同的CD封面,然后看着代理显示“加载中”,直到出现真正的图像。
  2. 画面出现“加载中”消息时,缩放窗口大小,注意到代理会在不挂起Swing窗口的情况下处理加载。
  3. 在ImageProxyTestDrive中,加入一些你自己喜欢的CD。

我们做了什么?

① 我们创建了一个用来显示的ImageProxy。paintIcon()方法会被调用,而ImageProxy会产生线程取得图像,并创建ImageIcon。

② 在某个时间点,图像被返回,ImageIcon被完整实例化。

③ 在ImageIcon被创建后,下次调用到paintIcon()时,代理就委托ImageIcon进行。

Q&A

问:对我来说,远程服务器和虚拟服务器差异非常大,它们真的是一个模式吗?

答:在真实的世界中,代理模式有许多变体,这些变体都有共通点:都会将客户对主题(Subject)施加的方法调用拦截下来。这种间接的级别让我们可以做许多事,包括将请求分发到远程主题;给创建开销大的对象提供代表,或者正如你将要看到的,提供某些级别的保护,这种保护能决定哪些客户能调用哪些方法。这还只是个开端,其实一般的代理模式还可以以许多形式使用。

问:ImageProxy在我看来好像是Decorator(装饰者)。我的意思是,我们基本上都是用一个对象把另一个包起来,然后把调用委托给ImageIcon。我这样说有什么问题吗?

答:有时候这两者的确看起来很像,但是它们的目的是不一样的。装饰者为对象增加行为,而代理是控制对象的访问。你可能会说:“显示‘加载中’消息,难道就不是在增加行为?”。从某方面来说,这的确可以算是,但是,更重要的ImageProxy是控制ImageIcon的访问。如何控制呢?试想:代理将客户从ImageIcon解耦了,如果它们之间没有解耦,客户就必须等到每幅图像都被取回,然后才能把它绘制在界面上。代理控制ImageIcon的访问,以便在图像完全创建之前提供屏幕上的代表。一旦ImageIcon被创建,代理就允许访问ImageIcon。

问:我要如何让客户使用代理,而不是真正的对象?

答:好问题。一个常用的技巧是提供一个工厂,实例化并返回主题。因为这是在工厂方法内发生的,我们可以用代理包装主题再返回,而客户不知道也不在乎他使用的是代理还是真东西。

问:我注意到,在ImageProxy的例子中,你总是创建新的ImageProxy来取得图像,即使图像已经被取回来过。能不能把加载过的图像放在缓存中呢?

答:你说的是缓存代理(Caching Proxy)。缓存代理会维护之前创建的对象,当收到请求时,在可能的情况下返回缓存对象。

问:我已经知道代理和装饰者的关系了,但是适配器呢?代理和适配器也很类似。

答:代理和适配器都是挡在其他对象的前面,并负责将请求转发给它们。适配器会改变对象适配的接口,而代理则实现相同的接口。

有一个额外相似性牵涉到保护代理(Protection Proxy)。保护代理可以根据客户的角色来决定是否允许客户访问特定的方法。所以保护代理可能只提供给客户部分接口,这就和某些适配器很相像了。

代理模式和装饰者模式比较

代理模式:

  • 代表真实的对象,为对象加上动作,避免不想要的访问或隐藏主题在远程运行的事实。

装饰者模式:

  • 为对象增加行为,这会改变对象的行为

使用Java API的代理,创建一个保护代理

Java在java.lang.reflect包中有自己的代理支持,利用这个包你可以在运行时动态地创建一个代理类,实现一个或多个接口,并将方法的调用转发到你指定的类。因为实际的代理是在运行时创建的,我们称这个Java技术为:动态代理。

我们要利用Java的动态代理创建我们下一个代理实现(保护代理)。但在这之前,先让我们看一下类图,了解一下动态代理是怎么一回事。就和真实世界中大多数的事物一样,它和代理模式的传统定义有一点出入。

因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。既然这样的代码不能放在Proxy类中年,那么要放在哪里?放在InvocationHandler中。InvocationHandler的工作是响应代理的任何调用。你可以把InvocationHandler想成是代理收到方法调用后,请求做实际工作的对象。

接下来,看看如何使用动态代理……

对象村的配对

每个城镇都需要配对服务,不是吗?你负责帮对象村实现约会服务系统。你又一个好点子,就是在服务中加入“Hot”和“Not”的评鉴,“Hot”就表示喜欢对象,“Not”表示不喜欢。你希望这套系统能鼓励你的顾客找到可能的配对对象,这也会让事情更有趣。

你的服务系统涉及到一个Person bean,允许设置或取得一个人的信息:

public interface PersonBean {

    /**
     * 这里我们可以取得人的名字,性别、兴趣和HotOrNot评分(1到10)
     * @return
     */
    String getName();
    String getGender();
    String getInterests();
    int getHotOrNotRating();

    /**
     * 通过调用各自的方法,我们也可以设置这些信息。
     * @param name
     */
    void setName(String name);
    void setGender(String gender);
    void setInterests(String interests);
    void setHotOrNotRating(int rating);
    
}

现在,让我们看看实现……

PersonBean的实现

public class PersonBeanImpl implements PersonBean {
    String name;
    String gender;
    String interests;
    int rating;
    int ratingCount = 0;


    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getGender() {
        return gender;
    }

    @Override
    public String getInterests() {
        return interests;
    }

    @Override
    public int getHotOrNotRating() {
        if(ratingCount == 0){
            return 0;
        }
        //计算rating的平均值
        return rating / ratingCount;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public void setInterests(String interests) {
        this.interests = interests;
    }

    /**
     * setHotOrNotRating()增加计数值
     * 并将参数加到rating实例变量中
     * @param rating
     */
    @Override
    public void setHotOrNotRating(int rating) {
        this.rating += rating;
        ratingCount++;
    }
}

Elory:我以前不太容易找到约会对象,后来我发现原来有人篡改过我的兴趣。我还发现有人居然给自己平高分,以拉高自己的HotOrNotRating值。这真的是太卑鄙了!我认为系统不应该允许用户篡改别人的兴趣,也不应该允许让用户给自己打分数。

虽然外面怀疑Elory找不到约会对象可能是因为其他的因素,但是他说的没错,系统不应该允许用户篡改别人的数据。根据我们定义PersonBean的方式,任何客户都可以调用任何方法。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

转载请注明原文链接:ZJ-Wave

Top