草庐IT

设计模式---代理模式

spoonb's blog 2023-04-16 原文

简述

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

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

优化案例

最初版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 Map<String, byte[]> 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 Map<String, byte[]> 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 Map<String, byte[]> 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 Map<String, byte[]> 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 Map<String, byte[]> 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即可。减少了不必要的代码修改,提高了系统的可维护性。

总结

优点

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

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

缺点

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

适用场景

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

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

有关设计模式---代理模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  5. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  6. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  9. ruby-on-rails - 设计注册确认 - 2

    我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:

  10. ruby - HTTP 请求中的用户代理,Ruby - 2

    我是Ruby的新手。我试过查看在线文档,但没有找到任何有效的方法。我想在以下HTTP请求botget_response()和get()中包含一个用户代理。有人可以指出我正确的方向吗?#PreliminarycheckthatProggitisupcheck=Net::HTTP.get_response(URI.parse(proggit_url))ifcheck.code!="200"puts"ErrorcontactingProggit"returnend#Attempttogetthejsonresponse=Net::HTTP.get(URI.parse(proggit_url)

随机推荐