设计模式学习笔记(十五):代理模式

文章目录

  • 1 概述
    • 1.1 引言
    • 1.2 定义
    • 1.3 结构图
    • 1.4 角色
    • 1.5 分类
    • 1.6 与装饰模式的不同
  • 2 典型实现
    • 2.1 步骤
    • 2.2 抽象主题角色
    • 2.3 真实主题角色
    • 2.4 代理主题角色
    • 2.5 客户端
  • 3 实例
  • 4 动态与静态代理
    • 4.1 静态代理
    • 4.2 动态代理
      • 4.2.1 `Proxy`
      • 4.2.2 `InvocationHandler`
      • 4.2.3 实例
  • 5 远程代理
    • 5.1 概述
    • 5.2 RMI简例
  • 6 虚拟代理
    • 6.1 概述
    • 6.2 适用情况
    • 6.3 优缺点
    • 6.4 简例
  • 7 缓存代理
    • 7.1 抽象主题角色
    • 7.2 真实主题角色
    • 7.3 代理主题角色
    • 7.4 其他
    • 7.5 测试
  • 8 主要优点
  • 9 主要缺点
  • 10 适用场景
  • 11 总结

1 概述

1.1 引言

所谓代购,就是找人帮忙购买自己需要的商品,代购包括两种类型,一种是在当地买不到商品,或者因为当地该商品价格较高,因此托人在其他地区或者国外购买,另一种类型是消费者对想要购买的商品消息缺乏,只能委托中介或者中间商购买。

在软件开发中,有时也需要提供与代购类似的功能,由于某些原因,客户端不想或不能直接访问对象,此时可通过一种叫代理的第三者来实现间接访问,这种方案对应的设计模式称为代理模式。

代理模式是一种应用很广泛的结构型设计模式,而且变化很多。在代理模式中引入了一个新的代理对象,代理对象可以在客户端对象和目标对象之间起到中介的作用,去掉客户不能看到的内容或者增添客户需要的额外服务。

1.2 定义

代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

代理模式是一种对象结构型模式。

1.3 结构图

设计模式学习笔记(十五):代理模式_第1张图片

1.4 角色

  • Subject(抽象主题角色):声明了真实主题和代理主题的共同接口,客户端通常需要针对抽象主题角色编程
  • Proxy(代理主题角色):内部包含了对真实主题的引用,从而可以操作真实主题对象。代理主题角色中提供了一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题。代理角色还可以控制对真实主题的使用,在需要的时候创建或删除真实主题对象,并对真实主题的使用加以约束。通常在代理主题角色中,客户端调用之前或之后都需要执行特定操作,比如图中的preRequest以及postRequest
  • RealSubject(真实主题角色):定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理角色间接调用真实主题角色中的操作

1.5 分类

代理模式根据目的以及实现方式可以分成很多类,常见的几种如下:

  • 远程代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以不在同一台主机中。远程代理又叫“大使”(Ambassador)
  • 虚拟代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
  • 保护代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
  • 缓存代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
  • 智能引用代理:当一个对象被引用时,提供一些额外的操作,比如将对象被调用的次数记录下来等

1.6 与装饰模式的不同

代理模式和装饰模式在实现时类似,主要区别如下:

  • 增加的职责范围问题域不同:代理模式以及装饰模式都能动态地增加职责,但是代理模式增加的是一些全新的职责,比如权限控制,缓存处理,智能引用,远程访问等,这些职责与原有职责不属于同一个问题域。对于装饰模式,为具体构件类增加一些相关的职责,是原有职责的扩展,这些职责属于同一个问题域
  • 目的不同:代理模式的目的是控制对对象的访问,而装饰模式是为对象动态增加功能

2 典型实现

2.1 步骤

  • 定义抽象主题角色:定义为抽象类/接口,声明抽象业务方法
  • 定义真实主题角色:继承/实现抽象主题角色,实现真实业务操作
  • 定义代理主题角色:继承/实现抽象主题角色,将客户端的请求转发到真实主题角色进行调用,同时根据需要进行调用前/后的一些相关操作

2.2 抽象主题角色

这里简单实现为一个接口:

interface Subject
{
    void request();
}

2.3 真实主题角色

实现抽象主题接口,执行真正的业务操作:

class RealSubject implements Subject
{
    public void request()
    {
        System.out.println("真实主题角色方法");
    }
}

2.4 代理主题角色

同样实现抽象主题接口,一般来说在调用真正的业务方法之前或之后会有相关操作:

class Proxy implements Subject
{
    private RealSubject subject = new RealSubject();
    public void pre()
    {
        System.out.println("代理前操作");
    }

    public void request()
    {
        pre();
        subject.request();
        post();
    }

    public void post()
    {
        System.out.println("代理后操作");
    }
}

2.5 客户端

客户端针对抽象主题角色进行编程即可,如果不需要代理,则实例化真实主题角色,如果需要代理则实例化代理主题角色:

public static void main(String[] args) 
{
    Subject subject = new RealSubject();
    subject.request();
    System.out.println("\n使用代理:\n");
    subject = new Proxy();
    subject.request();
}

设计模式学习笔记(十五):代理模式_第2张图片

3 实例

一个已具有搜索功能的系统,需要为搜索添加身份认证以及日志记录功能,使用代理模式设计该系统。

设计如下:

  • 抽象主题角色:Searcher
  • 真实主题角色:RealSearcher
  • 代理主题角色:ProxySearcher

代码如下:

public class Test
{
    public static void main(String[] args) 
    {
        Searcher subject = new ProxySearcher();
        subject.search();
    }
}

interface Searcher
{
    void search();
}

class RealSearcher implements Searcher
{
    public void search()
    {
        System.out.println("搜索");
    }
}

class ProxySearcher implements Searcher
{
    private RealSearcher subject = new RealSearcher();
    public void validate()
    {
        System.out.println("身份验证");
    }

    public void search()
    {
        validate();
        subject.search();
        log();
    }

    public void log()
    {
        System.out.println("日志记录,查询次数+1");
    }
}

进行搜索之前,先验证用户,接着进行搜索,搜索完成后进行日志记录,这是保护代理以及智能引用代理的例子。

4 动态与静态代理

4.1 静态代理

通常情况下,每一个代理类编译之后都会生成一个字节码文件,代理所实现的接口和所代理的方法都固定,这种代理称为静态代理。

静态代理中,客户端通过Proxy调用RealSubjectrequest方法,同时封装其他方法(代理前/代理后操作),比如上面的查询验证以及日志记录功能。

静态代理的优点是实现简单,但是,代理类以及真实主题类都需要事先存在,代理类的接口以及代理方法都明确指定,但是如果需要:

  • 代理不同的真实主题类
  • 代理一个真实主题类的不同方法

需要增加新的代理类,这会导致系统中类的个数大大增加。

这是静态代理最大的缺点,为了减少系统中类的个数,可以采用动态代理。

4.2 动态代理

动态代理可以让系统根据实际需要动态创建代理类,同一个代理类可以代理多个不同的真实主题类,而且可以代理不同方法,在Java中实现动态代理需要Proxy类以及InvocationHandler接口。

4.2.1 Proxy

Proxy类提供了用于创建动态代理类和实例对象的方法,最常用的方法包括:

  • public static Class getProxy(ClassLoader loader,Class ... interfaces):该方法返回一个Class类型的代理类,在参数中需要提供类加载器并指定代理的接口数组,这个数组应该与真实主题类的接口列表一致
  • public staitc Object newProxyInstance(ClassLoader loader,Class [] interfaces,InvocationHandler h):返回一个动态创建的代理类实例,第一个参数是类加载器,第二个参数表示代理类实现的接口列表,同理与真实主题的接口列表一致,第三个参数表示h所指派的调用处理程序类

4.2.2 InvocationHandler

InvocationHandler接口是代理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用者(也就是实现了InvocationHandler的类),该接口中声明以下方法:

  • public Object invoke(Object proxy,Method method,Object [] args):该方法用于处理对代理类实例的方法调用并返回相应结果,当一个代理实例中的业务方法被调用时自动调用该方法。第一个参数表示代理类的实例,第二个参数表示需要代理的方法,第三个参数表示方法的参数数组

动态代理类需要在运行时指定所代理的真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会自动转发到InvocationHandlerinvoke方法,由invoke实现对请求的统一处理。

4.2.3 实例

为一个数据访问Dao层增加方法调用日志,记录每一个方法被调用的时间和结果,使用动态代理模式进行设计。

设计如下:

  • 抽象主题角色:AbstractUserDao
  • 真实主题角色:UserDao1+UserDao2
  • 请求处理角色:DAOLogHandler
  • 代理主题角色:无需手动定义,由Proxy.newInstance()生成

首先设计抽象主题角色:

interface AbstarctUserDao
{
    void findUserById(String id);
}

接着创建两个具体类实现该接口:

class UserDao1 implements AbstarctUserDao
{
    public void findUserById(String id)
    {
        System.out.println("1号数据库中查找id" + 
            ("1".equals(id) ? "成功" : "失败"));
    }
}


class UserDao2 implements AbstarctUserDao
{
    public void findUserById(String id)
    {
        System.out.println("2号数据库中查找id" + 
            ("2".equals(id) ? "成功" : "失败"));
    }
}

接着定义请求处理角色:

class DAOLogHandler implements InvocationHandler
{
    private Object object;
    public DAOLogHandler(Object object)
    {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
    {
        beforeInvoke();
        Object result = method.invoke(object, args);
        postInvoke();
        return result;
    }

    private void beforeInvoke()
    {
        System.out.println("记录时间");
    }

    private void postInvoke()
    {
        System.out.println("记录结果");
    }
}

核心是实现了InvocationHandlerinvoke方法,该方法在调用抽象主题角色中的方法时自动转发到该方法处理。

也就是说,假设抽象主题角色有A(),B(),C()三个方法,当调用A()时,将调用A()替换掉里面的Object result = method.invoke(object.args),也就是实际上相当调用如下函数:

@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
    beforeInvoke();
    Object result = A(args);
    postInvoke();
    return result;
}

当调用B()时,相当于调用以下函数:

@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
    beforeInvoke();
    Object result = B(args);
    postInvoke();
    return result;
}

下面是测试客户端的代码:

public static void main(String[] args) 
{
    AbstarctUserDao userDao1 = new UserDao1();
    AbstarctUserDao proxy = null;
    InvocationHandler handler = new DAOLogHandler(userDao1);
    proxy = AbstarctUserDao.class.cast(
        Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
    );
    proxy.findUserById("2");

    AbstarctUserDao userDao2 = new UserDao2();
    handler = new DAOLogHandler(userDao2);
    proxy = AbstarctUserDao.class.cast(
        Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(),new Class[]{AbstarctUserDao.class},handler)
    );
    proxy.findUserById("2");
}

输出如下:
设计模式学习笔记(十五):代理模式_第3张图片
在测试代码中,代理主题角色由以下语句生成:

proxy = AbstarctUserDao.class.cast(
    Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
);

其中cast()方法相当于是对强制类型转换进行了包装,转换前进行了安全检查。

Proxy.newInstance()中,第一个参数是抽象主题角色的类加载器,第二个参数表示抽象主题角色的所有方法都转发请求到第三个参数中的invoke方法处理。第三个参数是自定义的InvocationHandler,通过构造方法注入抽象主题角色,目的是提供一个抽象主题角色的引用,调用代理方法时自动调用抽象主题角色的方法。

5 远程代理

5.1 概述

远程代理是一种常见的代理模式,使得客户端程序可以访问在远程主机(或另一个JVM)上的对象,远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是本地的而不是远程的,远程代理对象承担了大部分的网络通信工作,并负责对远程业务的方法调用。

远程业务对象在本地主机中有一个代理对象,该代理对象负责对远程业务对象的访问和网络通信,它对于客户端而言是透明的。客户端无须关心实现的具体业务是谁,只需要按照服务接口所定义的方式直接与本地主机中的代理对象交互即可。

在Java中可以通过RMI(Remote Method Invocation,远程方法调用)机制来实现远程代理,它能够实现一个JVM中的对象调用另一个JVM中的对象,下面看一个简单的例子。

5.2 RMI简例

这个简单的例子有以下四个类:

  • 接口:Hello
  • 接口实现类:HelloImpl
  • 服务端:HelloServer
  • 客户端:HelloClient

代码如下:

interface Hello extends Remote
{
    String sayHello(String name) throws RemoteException;
}

一个简单的sayHello方法,注意里面的方法需要声明为抛出RemoteException

接着是接口实现类:

public class HelloImpl extends UnicastRemoteObject implements Hello{
    public HelloImpl() throws RemoteException
    {
        super();
    }
    public String sayHello(String name) throws RemoteException
    {
        System.out.println("Hello");
        return "Hello"+name;
    }
}

实现sayHello方法。

接下来是服务端:

public class HelloServer {
    public static void main(String[] args) {
        try {
            Hello hello = new HelloImpl();
            LocateRegistry.createRegistry(8888);
            System.setProperty("java.rmi.server.hostname", "127.0.0.1");
            Naming.bind("rmi://localhost:8888/hello", hello);
            System.out.println("远程绑定对象成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服务端中首先注册了一个本地端口8888,接着设置系统属性rmi服务的主机名为本地地址,也就是127.0.0.1,如果是部署在服务器上修改对应ip即可。下一步是通过Naming的静态方法bind绑定该URL到RMI服务器上,并命名为hello。其中rmi:(RMI协议)可以省略。

最后是客户端:

public class HelloClient {
    public static void main(String[] args) {
        try
        {
            Hello hello = Hello.class.cast(
                Naming.lookup("rmi://127.0.0.1:8888/hello")
            );
            System.out.println(hello.sayHello("111"));
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}

客户端通过Naminglookup查找参数URL中对应的远程服务对象hello,找到后返回并强制转换为Hello,接着即可调用远程对象的方法sayHello

首先运行服务端:
在这里插入图片描述
接着启动客户端:
在这里插入图片描述
可以看到来自服务端的结果。

再查看服务端:
在这里插入图片描述
可以看到这是调用了sayHello的结果。

6 虚拟代理

6.1 概述

对于一直占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮
演真实对象的替身,当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。

6.2 适用情况

以下两种情况可以考虑使用虚拟代理:

  • 由于对象本身复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象
  • 当一个对象的加载十分消耗系统资源的时候,也非常适合使用虚拟代理。虚拟代理可以让那些占用大量内存或处理起来非常复杂的对象推迟到使用到它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象

6.3 优缺点

  • 优点:由于应用程序启动时由于不需要创建和装载所有的对象,因此加速了应用程序的启动
  • 缺点:不能保证特定的应用程序对象被创建,在访问这个对象的任何地方都需要提前进行判空操作

6.4 简例

有一批人找老板谈事情,谈事情之前需要先通过老板的助手进行预约,预约这件事只需要助手完成,真正执行预约列表里面的任务时才需要老板出现,使用虚拟代理模式进行设计。

设计如下:

  • 抽象主题角色:Approvable
  • 真实主题角色:Boss
  • 代理主题角色:Assistant

代码如下:

//抽象主题角色
interface Approvable
{
    void approve();
}

下一步定义真实主题角色Boss

class Boss implements Approvable
{
    private List<String> orders = new LinkedList<>();
    
    static
    {
        System.out.println("\n老板来处理了\n");
    }

    public Boss(List<String> orders)
    {
        this.orders = orders;
    }

    public void addOrder(String order)
    {
        orders.add(order);
    }

    @Override
    public void approve()
    {
        while(orders.size() > 0)
        {
            System.out.println("老板处理了<"+orders.remove(0)+">");
        }
    }
}

使用List存储待处理的事件,approve表示处理所有的事件。

代理主题角色如下:

class Assistant implements Approvable
{
    private List<String> orders = new LinkedList<>();
    private volatile Boss boss;

    public void addOrder(String order)
    {
        if(boss != null)
        {
            System.out.println("老板将<"+order+">添加到预约列表");
            boss.addOrder(order);
        }
        else
        {
            System.out.println("助手将<"+order+">添加到预约列表");
            orders.add(order);
        }
    }

    @Override
    public void approve()
    {
        if(boss == null)
        {
            synchronized(this)
            {
                if(boss == null)
                {
                    boss = new Boss(orders);
                }
            }
        }
        boss.approve();       
    }
}

在添加事件(addOrder)函数中,首先判断boss是否为null,如果为null表示还没创建老板对象,这时让助手添加到预约列表中去,如果不为null表示已经存在老板对象,直接交由老板加入预约列表。

对于approve方法,首先判断boss是否为null,不为null表示老板能直接处理所有事件。为null表示老板对象还没有创建,新建一个Boss并将待处理的事件作为参数注入boss中。

测试类:

public static void main(String[] args) 
{
    Assistant assistant = new Assistant();
    assistant.addOrder("找老板面试");
    assistant.addOrder("找老板借钱");
    assistant.addOrder("找老板聊天");
    assistant.approve();

    assistant.addOrder("找老板吃饭");
    assistant.addOrder("找老板喝酒");
    assistant.approve();
}

输出如下:

设计模式学习笔记(十五):代理模式_第4张图片

7 缓存代理

缓存代理为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果,在这里使用缓存代理模式模拟YouTube对使用集成的第三方库下载进行缓存。

设计如下:

  • 模拟第三方库:ThirdPartyYouTubeLib+ThirdPartyYouTubeClass
  • 模拟视频文件:Video
  • 模拟缓存代理:YouTubeCacheProxy
  • 模拟下载器:YouTubeDownloader

首先是第三方类库,通常情况下是没有源码实现的,其中ThirdPartyYouTubeLib是一个接口,并且ThirdPartyYouTubeClass以及YouTubeCacheProxy实现了它,也就是说:

  • ThirdPartyYouTubeLib是抽象主题角色
  • ThirdPartyYouTubeClass是真实主题角色
  • YouTubeCacheProxy是代理主题角色

7.1 抽象主题角色

首先定义抽象主题角色:

interface ThirdPartyYouTubeLib
{
    HashMap<String,Video> popularVideos();
    Video getVideo(String videoId);
}

一个是获取热门视频的方法,一个是根据id获取具体视频的方法。

7.2 真实主题角色

class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib
{
    private static final String URL = "https://www.youtube.com";
    @Override
    public HashMap<String,Video> popularVideos()
    {
        connectToServer(URL);
        return getRandomVideos();
    }

    @Override
    public Video getVideo(String id)
    {
        connectToServer(URL+id);
        return getSomeVideo(id);
    }

    private int random(int min,int max)
    {
        return min+(int)(Math.random()*((max-min)+1));
    }

    private void experienceNetworkLatency()
    {
        int randomLatency = random(5, 10);
        for(int i=0;i<randomLatency;++i)
        {
            try
            {
                Thread.sleep(100);
            } 
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    private void connectToServer(String url)
    {
        System.out.println("连接到 " + url + " ...");
        experienceNetworkLatency();
        System.out.println("连接成功!\n");
    }

    private HashMap<String,Video> getRandomVideos()
    {
        System.out.println("正在下载热门视频");
        experienceNetworkLatency();
        HashMap<String,Video> map = new HashMap<>();
        map.put("1111111",new Video("1111","1111.mp4"));
        map.put("2222222",new Video("2222","2222.avi"));
        map.put("3333333",new Video("3333","3333.mov"));
        map.put("4444444",new Video("4444","4444.mkv"));
        System.out.println("下载完成!\n");
        return map;
    }

    private Video getSomeVideo(String id)
    {
        System.out.println("正在下载id为"+id+"的视频");
        experienceNetworkLatency();
        System.out.println("下载完成!\n");
        return new Video(id,"title");
    }
}

获取热门视频或者某一个视频时,进行了一个模拟连接到服务器的操作,首先输出提示连接到xxx,接着模拟了网络延迟,最后提示下载完成并返回相应的视频。

7.3 代理主题角色

class YouTubeCacheProxy implements ThirdPartyYouTubeLib
{
    private ThirdPartyYouTubeLib youtubeService = new ThirdPartyYouTubeClass();

    private HashMap<String,Video> cachePopular = new HashMap<>();
    private HashMap<String,Video> cacheAll = new HashMap<>();

    @Override
    public HashMap<String,Video> popularVideos()
    {
        if(cachePopular.isEmpty())
        {
            cachePopular = youtubeService.popularVideos();
        }
        else
        {
            System.out.println("从缓存检索中热门视频");            
        }
        return cachePopular;
    }

    @Override
    public Video getVideo(String id)
    {
        Video video = cacheAll.get(id);
        if(video == null)
        {
            video = youtubeService.getVideo(id);
            cacheAll.put(id,video);
        }
        else
        {
            System.out.println("从缓存中检索id为"+id+"的视频");
        }
        return video;
    }

    public void reset()
    {
        cachePopular.clear();
        cacheAll.clear();
    }
}

这里的缓存代理角色其实就是在调用真实主题角色的获取视频方法之前,首先判断是否存在缓存,存在的话直接从缓存中获取,不存在的话首先调用获取视频方法并存储在缓存中,下次获取时从缓存中获取。

7.4 其他

class Video
{
    private String id;
    private String title;
    private String data;
    public Video(String id,String title)
    {
        this.id = id;
        this.title = title;
    }
    //getter+setter...
}

class YouTubeDownloader
{
    private ThirdPartyYouTubeLib api;

    public YouTubeDownloader(ThirdPartyYouTubeLib api)
    {
        this.api = api;
    }

    public boolean useCacheProxy()
    {
        return api instanceof YouTubeCacheProxy;
    }

    public void renderVideoPage(String id)
    {
        Video video = api.getVideo(id);
        System.out.println("\n-------------------------------------------");
        System.out.println("ID:"+video.getId());
        System.out.println("标题:"+video.getTitle());
        System.out.println("数据:"+video.getData());
        System.out.println("\n-------------------------------------------");
    }

    public void renderPopularVideos()
    {
        HashMap<String,Video> list = api.popularVideos();
        System.out.println("\n-------------------------------------------");
        System.out.println("热门视频");
        list.forEach((k,v)->System.out.println("ID:"+v.getId()+"\t标题:"+v.getTitle()));
        System.out.println("\n-------------------------------------------");
    }
}

7.5 测试

public class Test
{
    public static void main(String[] args) 
    {
        YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
        YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());

        long navie = test(naiveDownloader);
        long smart = test(smartDownloader);
        System.out.println("缓存代理节约的时间:"+(navie-smart)+"ms");
    }

    private static long test(YouTubeDownloader downloader)
    {
        long startTime = System.currentTimeMillis();
        downloader.renderPopularVideos();
        downloader.renderVideoPage("1111");
        downloader.renderPopularVideos();
        downloader.renderVideoPage("2222");
        downloader.renderVideoPage("3333");
        downloader.renderVideoPage("4444");
        long estimatedTime = System.currentTimeMillis() - startTime;
        System.out.println(downloader.useCacheProxy() ? "使用缓存运行时间:" : "不使用缓存运行时间:");
        System.out.println(estimatedTime+"ms\n");
        return estimatedTime;
    }
}

模拟了两个下载器,一个使用原生下载,一个使用缓存代理下载,输出如下:

连接到 https://www.youtube.com ...
连接成功!

正在下载热门视频
下载完成!


-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4

-------------------------------------------
连接到 https://www.youtube.com1111 ...
连接成功!

正在下载id为1111的视频
下载完成!


-------------------------------------------
ID:1111
标题:title
数据:null

-------------------------------------------
连接到 https://www.youtube.com ...
连接成功!

正在下载热门视频
下载完成!


-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4

-------------------------------------------
连接到 https://www.youtube.com2222 ...     
连接成功!

正在下载id为2222的视频
下载完成!


-------------------------------------------
ID:2222
标题:title
数据:null

-------------------------------------------
连接到 https://www.youtube.com3333 ...     
连接成功!

正在下载id为3333的视频
下载完成!


-------------------------------------------
ID:3333
标题:title
数据:null

-------------------------------------------
连接到 https://www.youtube.com4444 ...     
连接成功!

正在下载id为4444的视频
下载完成!


-------------------------------------------
ID:4444
标题:title
数据:null

-------------------------------------------
不使用缓存运行时间:
9312ms

连接到 https://www.youtube.com ...
连接成功!

正在下载热门视频
下载完成!


-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4

-------------------------------------------
连接到 https://www.youtube.com1111 ...
连接成功!

正在下载id为1111的视频
下载完成!


-------------------------------------------
ID:1111
标题:title
数据:null

-------------------------------------------
从缓存检索中热门视频

-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4

-------------------------------------------
连接到 https://www.youtube.com2222 ...
连接成功!

正在下载id为2222的视频
下载完成!


-------------------------------------------
ID:2222
标题:title
数据:null

-------------------------------------------
连接到 https://www.youtube.com3333 ...
连接成功!

正在下载id为3333的视频
下载完成!


-------------------------------------------
ID:3333
标题:title
数据:null

-------------------------------------------
连接到 https://www.youtube.com4444 ...
连接成功!

正在下载id为4444的视频
下载完成!


-------------------------------------------
ID:4444
标题:title
数据:null

-------------------------------------------
使用缓存运行时间:
7611ms

缓存代理节约的时间:1701ms

可以看到缓存代理是能节省时间的,除了第一次获取视频外,随后的获取视频都是从缓存中直接提取。

8 主要优点

  • 降低耦合度:代理模式能够协调调用者以及被调用者,一定程度上降低了系统的耦合度
  • 灵活可扩展:客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源码,符合OCP,系统具有较好的灵活性和可扩展性
  • 提高整体效率(远程代理):远程代理为位于两个不同的地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统整体运行效率
  • 节约开销(虚拟代理):虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
  • 控制权限(保护代理):保护代理可以控制一个对象的访问权限,为不同用户提供不同级别的使用权限

9 主要缺点

  • 速度变慢:由于在客户端以及真实主题之间增加了代理对象,因此可能会造成处理速度变慢,比如保护代理
  • 实现复杂:实现代理模式需要额外的操作,有些代理模式其实很复杂,比如远程代理

10 适用场景

  • 客户端需要访问远程主机中的对象,使用远程代理
  • 需要一个消耗资源较少的对象来代表资源较多的对象时,使用虚拟代理
  • 需要控制访问权限,使用保护代理
  • 需要为一个频繁访问的操作结果提供临时存储空间,使用缓存代理
  • 需要为一个对象的访问(引用)提供额外的操作时,使用智能引用代理

11 总结

如果觉得文章好看,欢迎点赞。

同时欢迎关注微信公众号:氷泠之路。

在这里插入图片描述

你可能感兴趣的:(设计模式)