代理模式是一种结构型设计模式让你能够提供对象的替代品或其占位符代理控制着对于原对象的访问并允许在将请求提交给对象前后进行一些处理

代理设计模式

问题

为什么要控制对于某个对象的访问呢举个例子有这样一个消耗大量系统资源的巨型对象你只是偶尔需要使用它并非总是需要

代理模式解决的问题

数据库查询有可能会非常缓慢

你可以实现延迟初始化在实际有需要时再创建该对象对象的所有客户端都要执行延迟初始代码不幸的是这很可能会带来很多重复代码

在理想情况下我们希望将代码直接放入对象的类中但这并非总是能实现比如类可能是第三方封闭库的一部分

解决方案

代理模式建议新建一个与原服务对象接口相同的代理类然后更新应用以将代理对象传递给所有原始对象客户端代理类接收到客户端请求后会创建实际的服务对象并将所有工作委派给它

代理模式的解决方案

代理将自己伪装成数据库对象可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作

这有什么好处呢如果需要在类的主要业务逻辑前后执行一些工作你无需修改类就能完成这项工作由于代理实现的接口与原类相同因此你可将其传递给任何一个使用实际服务对象的客户端

真实世界类比

信用卡是一大捆现金的代理

信用卡和现金在支付过程中的用处相同

信用卡是银行账户的代理银行账户则是一大捆现金的代理它们都实现了同样的接口均可用于进行支付消费者会非常满意因为不必随身携带大量现金商店老板同样会十分高兴因为交易收入能以电子化的方式进入商店的银行账户中无需担心存款时出现现金丢失或被抢劫的情况

代理模式结构

代理设计模式的结构

  1. 服务接口Service Interface声明了服务接口代理必须遵循该接口才能伪装成服务对象

  2. 服务Service类提供了一些实用的业务逻辑

  3. 代理Proxy类包含一个指向服务对象的引用成员变量代理完成其任务例如延迟初始化记录日志访问控制和缓存等后会将请求传递给服务对象

    通常情况下代理会对其服务对象的整个生命周期进行管理

  4. 客户端Client能通过同一接口与服务或代理进行交互所以你可在一切需要服务对象的代码中使用代理

伪代码

尽管代理模式在绝大多数 Java 程序中并不常见但它在一些特殊情况下仍然非常方便当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时该模式是无可替代的

Java 标准程序库中的一些代理模式的示例

  • java.lang.reflect.Proxy

  • java.rmi.*

  • javax.ejb.EJB查看评论

  • javax.inject.Inject查看评论

  • javax.persistence.PersistenceContext

识别方法代理模式会将所有实际工作委派给一些其他对象除非代理是某个服务的子类否则每个代理方法最后都应该引用一个服务对象

缓存代理

在本例中代理模式有助于实现延迟初始化并对低效的第三方 YouTube 集成程序库进行缓存

当你需要在无法修改代码的类上新增一些额外行为时代理模式的价值无可估量

some_cool_media_library

some_cool_media_library/ThirdPartyYouTubeLib.java: 远程服务接口

package refactoring_guru.proxy.example.some_cool_media_library;

import java.util.HashMap;

public interface ThirdPartyYouTubeLib {
    HashMap<String, Video> popularVideos();

    Video getVideo(String videoId);
}

some_cool_media_library/ThirdPartyYouTubeClass.java: 远程服务实现

package refactoring_guru.proxy.example.some_cool_media_library;

import java.util.HashMap;

public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {

    @Override
    public HashMap<String, Video> popularVideos() {
        connectToServer("http://www.youtube.com");
        return getRandomVideos();
    }

    @Override
    public Video getVideo(String videoId) {
        connectToServer("http://www.youtube.com/" + videoId);
        return getSomeVideo(videoId);
    }

    // -----------------------------------------------------------------------
    // Fake methods to simulate network activity. They as slow as a real life.

    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 ex) {
                ex.printStackTrace();
            }
        }
    }

    private void connectToServer(String server) {
        System.out.print("Connecting to " + server + "... ");
        experienceNetworkLatency();
        System.out.print("Connected!" + "\n");
    }

    private HashMap<String, Video> getRandomVideos() {
        System.out.print("Downloading populars... ");

        experienceNetworkLatency();
        HashMap<String, Video> hmap = new HashMap<String, Video>();
        hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
        hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
        hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
        hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
        hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));

        System.out.print("Done!" + "\n");
        return hmap;
    }

    private Video getSomeVideo(String videoId) {
        System.out.print("Downloading video... ");

        experienceNetworkLatency();
        Video video = new Video(videoId, "Some video title");

        System.out.print("Done!" + "\n");
        return video;
    }

}

some_cool_media_library/Video.java: 视频文件

package refactoring_guru.proxy.example.some_cool_media_library;

public class Video {
    public String id;
    public String title;
    public String data;

    Video(String id, String title) {
        this.id = id;
        this.title = title;
        this.data = "Random video.";
    }
}

proxy

proxy/YouTubeCacheProxy.java: 缓存代理

package refactoring_guru.proxy.example.proxy;

import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeClass;
import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeLib;
import refactoring_guru.proxy.example.some_cool_media_library.Video;

import java.util.HashMap;

public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
    private ThirdPartyYouTubeLib youtubeService;
    private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
    private HashMap<String, Video> cacheAll = new HashMap<String, Video>();

    public YouTubeCacheProxy() {
        this.youtubeService = new ThirdPartyYouTubeClass();
    }

    @Override
    public HashMap<String, Video> popularVideos() {
        if (cachePopular.isEmpty()) {
            cachePopular = youtubeService.popularVideos();
        } else {
            System.out.println("Retrieved list from cache.");
        }
        return cachePopular;
    }

    @Override
    public Video getVideo(String videoId) {
        Video video = cacheAll.get(videoId);
        if (video == null) {
            video = youtubeService.getVideo(videoId);
            cacheAll.put(videoId, video);
        } else {
            System.out.println("Retrieved video '" + videoId + "' from cache.");
        }
        return video;
    }

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

downloader

downloader/YouTubeDownloader.java: 媒体下载应用

package refactoring_guru.proxy.example.downloader;

import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeLib;
import refactoring_guru.proxy.example.some_cool_media_library.Video;

import java.util.HashMap;

public class YouTubeDownloader {
    private ThirdPartyYouTubeLib api;

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

    public void renderVideoPage(String videoId) {
        Video video = api.getVideo(videoId);
        System.out.println("\n-------------------------------");
        System.out.println("Video page (imagine fancy HTML)");
        System.out.println("ID: " + video.id);
        System.out.println("Title: " + video.title);
        System.out.println("Video: " + video.data);
        System.out.println("-------------------------------\n");
    }

    public void renderPopularVideos() {
        HashMap<String, Video> list = api.popularVideos();
        System.out.println("\n-------------------------------");
        System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
        for (Video video : list.values()) {
            System.out.println("ID: " + video.id + " / Title: " + video.title);
        }
        System.out.println("-------------------------------\n");
    }
}

Demo.java: 初始化代码

package refactoring_guru.proxy.example;

import refactoring_guru.proxy.example.downloader.YouTubeDownloader;
import refactoring_guru.proxy.example.proxy.YouTubeCacheProxy;
import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeClass;

public class Demo {

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

        long naive = test(naiveDownloader);
        long smart = test(smartDownloader);
        System.out.print("Time saved by caching proxy: " + (naive - smart) + "ms");

    }

    private static long test(YouTubeDownloader downloader) {
        long startTime = System.currentTimeMillis();

        // User behavior in our app:
        downloader.renderPopularVideos();
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderPopularVideos();
        downloader.renderVideoPage("dancesvideoo");
        // Users might visit the same page quite often.
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderVideoPage("someothervid");

        long estimatedTime = System.currentTimeMillis() - startTime;
        System.out.print("Time elapsed: " + estimatedTime + "ms\n");
        return estimatedTime;
    }
}

代理模式适合应用场景

使用代理模式的方式多种多样我们来看看最常见的几种

延迟初始化虚拟代理)。 如果你有一个偶尔使用的重量级服务对象一直保持该对象运行会消耗系统资源时可使用代理模式

你无需在程序启动时就创建该对象可将对象的初始化延迟到真正有需要的时候

访问控制保护代理)。 如果你只希望特定客户端使用服务对象这里的对象可以是操作系统中非常重要的部分而客户端则是各种已启动的程序包括恶意程序), 此时可使用代理模式

代理可仅在客户端凭据满足要求时将请求传递给服务对象

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

在这种情形中代理通过网络传递客户端请求负责处理所有与网络相关的复杂细节

记录日志请求日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时

代理可以在向服务传递请求前进行记录

缓存请求结果缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时特别是当返回结果的体积非常大时

代理可对重复请求所需的相同结果进行缓存还可使用请求参数作为索引缓存的键值

智能引用可在没有客户端使用某个重量级对象时立即销毁该对象

代理会将所有获取了指向服务对象或其结果的客户端记录在案代理会时不时地遍历各个客户端检查它们是否仍在运行如果相应的客户端列表为空代理就会销毁该服务对象释放底层系统资源

代理还可以记录客户端是否修改了服务对象其他客户端还可以复用未修改的对象

实现方式

  1. 如果没有现成的服务接口你就需要创建一个接口来实现代理和服务对象的可交换性从服务类中抽取接口并非总是可行的因为你需要对服务的所有客户端进行修改让它们使用接口备选计划是将代理作为服务类的子类这样代理就能继承服务的所有接口了

  2. 创建代理类其中必须包含一个存储指向服务的引用的成员变量通常情况下代理负责创建服务并对其整个生命周期进行管理在一些特殊情况下客户端会通过构造函数将服务传递给代理

  3. 根据需求实现代理方法在大部分情况下代理在完成一些任务后应将工作委派给服务对象

  4. 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务你可以在代理类中创建一个简单的静态方法也可以创建一个完整的工厂方法

  5. 可以考虑为服务对象实现延迟初始化

代理模式优缺点

  • 你可以在客户端毫无察觉的情况下控制服务对象

  • 如果客户端对服务对象的生命周期没有特殊要求你可以对生命周期进行管理

  • 即使服务对象还未准备好或不存在代理也可以正常工作

  • 开闭原则你可以在不对服务或客户端做出修改的情况下创建新代理

  • 代码可能会变得复杂因为需要新建许多类

  • 服务响应可能会延迟

与其他模式的关系

  • 适配器模式能为被封装对象提供不同的接口代理模式能为对象提供相同的接口装饰模式则能为对象提供加强的接口

  • 外观模式代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化代理与其服务对象遵循同一接口使得自己和服务对象可以互换在这一点上它与外观不同

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