• 设计模式---代理模式


    简述

    对客户端隐藏目标类,创建代理类拓展目标类,并且对于客户端隐藏功能拓展的细节,使得客户端可以像使用目标类一样使用代理类,面向代理(客户端只与代理类交互)。

    话不多说,看一个优化案例。

    优化案例

    最初版v0

    目前的功能是下载可以下载文件。

    public class BiliBiliDownloader {
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    return new byte[1024]; // 假装是下载文件的字节数组
    }
    }

    客户端调用代码,如下。

    public class Client {
    public static void main(String[] args) throws InterruptedException {
    BiliBiliDownloader bilidownloader = new BiliBiliDownloader();
    bilidownloader.download("/root/buzuweiqi/java_manual.txt");
    }
    }

    下载工具类对客户端完全暴露,客户端可以直接使用下载类实现下载,这实际上是无可厚非的。
    经过研究发现,这个下载类有一个问题:每次调用都肯定会下载新的文件,即便文件已经被下载过。

    为了解决这个问题,开发团队经过商讨已经有了一个初步的方案。看一下代码样例。

    修改版v1

    团队决定使用传统的修改方式(直接修改BiliBiliDownloader),认为这样最为的直观。确实,代码量少且未来可以预期修改不频繁时,传统的修改方案也未尝不是一个好的选择。

    public class BiliBiliDownloader {
    // 定义用来缓存数据的map对象
    private static Mapbyte[]> map = new HashMap<>();
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    if (map.containsKey(filePath)) {
    return map.get(filePath);
    }
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    byte[] res = new byte[1024]; // 假装这是下载后的字节数组
    map.put(filePath, res); // 加入缓存
    return res;
    }
    }

    客户端调用代码,还是和原来一样。

    public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
    BiliBiliDownloader downloader = new BiliBiliDownloader();
    downloader.download("/root/home/buzuweiqi/java_manual.txt");
    // 由于文件已经缓存,所以这次下载非常快
    downloader.download("/root/home/buzuweiqi/java_manual.txt");
    // 由于文件还未缓存,所以这次下载比较缓慢
    downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
    }

    到目前为止好像都没有啥不妥的地方。直到有一天,客户提出了新的需求:虽然现在只可以下载bilibili的文件(视频,音频,文章等),以后还想要下载youtube的文件。

    为了实现这个需求,以及方便以后同类的需求变更,是时候用上代理模式。

    修改版v2

    代理模式在使用的时候需要顶一个一个顶层接口,并且使得代理类和被代理类都实现这个接口。
    代理类中需要持有非代理类的一个对象。并且在调用代理类的功能前后可以根据业务需要拓展新的功能。

    public interface Downloader {
    byte[] download(String filePath) throws InterruptedException;
    }
    public class BiliBiliDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    return new byte[1024]; // 假装是下载文件的字节数组
    }
    }
    public class ProxyBiliBiliDownloader implements Downloader {
    private static Mapbyte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
    if (map.containsKey(filePath)) {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    return map.get(filePath);
    }
    byte[] res = downloader.download(filePath);
    map.put(filePath, res);
    return res;
    }
    }
    public class YoutubeDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载Youtube文件:%s%n", filePath);
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    return new byte[1024]; // 假装是下载文件的字节数组
    }
    }
    public class ProxyYoutubeDownloader implements Downloader {
    private static Mapbyte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
    if (map.containsKey(filePath)) {
    System.out.printf("正在下载Youtube文件:%s%n", filePath);
    return map.get(filePath);
    }
    byte[] res = downloader.download(filePath);
    map.put(filePath, res);
    return res;
    }
    }

    客户端的使用案例如下。

    public class Client {
    public static void main(String[] args) throws IOException, InterruptedException {
    Downloader downloader = new ProxyBiliBiliDownloader();
    downloader.download("/root/home/buzuweiqi/java_manual.txt");
    downloader = new ProxyYoutubeDownloader();
    downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
    }

    客户端不再依赖目标类,而是转而依赖代理类。
    代理模式使得增加相似需求时可以只增加一对实现类(目标类,代理类),而不用修改原本的类,符合开闭原则。

    实际上通常我们会使用一个更为简单的方式控制代理对象的创建:反射。

    修改版v3

    高层接口,实现的目标类、代理类依旧不变。

    public interface Downloader {
    byte[] download(String filePath) throws InterruptedException;
    }
    public class BiliBiliDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    return new byte[1024]; // 假装是下载文件的字节数组
    }
    }
    public class ProxyBiliBiliDownloader implements Downloader {
    private static Mapbyte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
    if (map.containsKey(filePath)) {
    System.out.printf("正在下载BiliBili文件:%s%n", filePath);
    return map.get(filePath);
    }
    byte[] res = downloader.download(filePath);
    map.put(filePath, res);
    return res;
    }
    }
    public class YoutubeDownloader implements Downloader {
    public byte[] download(String filePath) throws InterruptedException {
    System.out.printf("正在下载Youtube文件:%s%n", filePath);
    // 模拟文件下载,睡个10秒
    Thread.sleep(10000);
    return new byte[1024]; // 假装是下载文件的字节数组
    }
    }
    public class ProxyYoutubeDownloader implements Downloader {
    private static Mapbyte[]> map = new HashMap<>();
    private BiliBiliDownloader downloader = new BiliBiliDownloader();
    public byte[] download(String filePath) throws InterruptedException {
    if (map.containsKey(filePath)) {
    System.out.printf("正在下载Youtube文件:%s%n", filePath);
    return map.get(filePath);
    }
    byte[] res = downloader.download(filePath);
    map.put(filePath, res);
    return res;
    }
    }

    在客户端调用时,引入Java反射,通过反射创建具体的代理对象。
    config.prop文件中定义PROXY_NAME变量并指定需要反射创建的类的完整路径

    public class Client {
    public static void main(String[] args) throws Exception {
    Properties prop = new Properties();
    prop.load(new FileReader("src/resource/props/config.prop"));
    Downloader downloader = (Downloader) Class.forName(prop.getProperty("PROXY_NAME"))
    .getDeclaredConstructor().newInstance();
    downloader.download("/root/home/buzuweiqi/java_manual.txt");
    downloader = new ProxyYoutubeDownloader();
    downloader.download("/root/home/buzuweiqi/linux_manual.txt");
    }
    }

    通过Java反射机制,应对每次的需求变更,甚至都不需要修改客户端代码,只需要修改案例中的config.prop即可。减少了不必要的代码修改,提高了系统的可维护性。

    总结

    优点

    • 代理类与目标类的使用方式一致,这极大的降低了客户端调用的学习成本,易用性高。

    • 面向接口,无需在意实现的细节。

    缺点

    • 类的数量倍增,系统复杂度增加。

    适用场景

    • 当需要对于模块拓展,但又不方便打破客户端原有的调用规则时。客户端中对象的创建依旧需要修改,这没有什么好的办法。
    • 常用的代理模式使用方案
      • 缓冲代理(案例)
      • 远程代理
      • 虚拟代理

    除此之外还有很多应用场景,代理模式是设计模式中使用非常广泛的一种。

  • 相关阅读:
    了解JVM中的Server和Client参数
    “AI教父”Geoffrey Hinton:智能进化的下一个阶段
    基于单片机16位智能抢答器设计
    Python3 笔记:upper()、isupper()、lower()、islower()、swapcase()
    设计素材网站,我推荐这6个
    如何在报表开发工具 FastReport Online Designer 中处理报表的 5 个函数
    戏说领域驱动设计(十一)——纠偏
    [前端CSS高频面试题]如何画0.5px的边框线(详解)
    【毕业设计】大数据用户画像分析系统 - 数据分析
    视频声音怎么翻译?这几个办法教你实现视频声音翻译成中文
  • 原文地址:https://www.cnblogs.com/spoonb/p/16735362.html