设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
代理模式
> 代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。 ![代理设计模式](/media/202203/2022-03-05_2302300.6018231833278456.png) 在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。 在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。 ## 问题 为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。 你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。 在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。 ## 解决方案 代理模式建议**新建一个与原服务对象接口相同的代理类**, 然后更新应用以将代理对象传递给所有原始对象客户端。 **代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它**。 这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。 ## 目的 代理模式(Proxy)为其他对象提供一种代理以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制目标对象的访问(目标对象可以是远程的对象、创建开销大的对象或需要安全控制的对象),并且可以在不改变目标对象的情况下添加一些额外的功能。 在某些情况下,一个客户不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。 ## 介绍 **意图:** 为其他对象提供一种代理以控制对这个对象的访问。 **主要解决:** 在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。 **何时使用:** 想在访问一个类时做一些控制。 **如何解决:** 增加中间层。 **关键代码:** 实现与被代理类组合。 **应用实例:** 1. Windows 里面的快捷方式。 2. 猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。 3. 买火车票不一定在火车站买,也可以去代售点。 4. 一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。 5. spring aop。 **优点:** 1. 职责清晰。 2. 高扩展性。 3. 智能化。 **缺点:** 1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。 **使用场景:** 按职责来划分,通常有以下使用场景: 1. 远程代理。 2. 虚拟代理。 3. Copy-on-Write 代理。 4. 保护(Protect or Access)代理。 5. Cache 代理。 6. 防火墙(Firewall)代理。 7. 同步化(Synchronization)代理。 8. 智能引用(Smart Reference)代理。 **注意事项:** 1. 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。 2. 和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。 ## 结构 ![代理设计模式的结构](/media/202203/2022-03-05_2313080.3616177513016836.png) - 服务接口 (Service Interface) 声明了服务接口。 **代理必须遵循该接口才能伪装成服务对象**。 - 服务 (Service) 类提供了一些实用的业务逻辑。 - 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。 - 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。 ### 实例 我们将创建一个 *Image* 接口和实现了 *Image* 接口的实体类。*ProxyImage* 是一个代理类,减少 *RealImage* 对象加载的内存占用。 *ProxyPatternDemo* 类使用 *ProxyImage* 来获取要加载的 *Image* 对象,并按照需求进行显示。 ![代理模式的 UML 图](https://www.runoob.com/wp-content/uploads/2014/08/20211025-proxy.svg) ## 伪代码 本例演示如何使用代理模式在第三方腾讯视频 (TencentVideo, 代码示例中记为 TV) 程序库中添加延迟初始化和缓存。 ![代理模式示例的结构](/media/202203/2022-03-05_2317160.012717073331697692.png) 程序库提供了视频下载类。 但是该类的效率非常低。 如果客户端程序多次请求同一视频, 程序库会反复下载该视频, 而不会将首次下载的文件缓存下来复用。 代理类实现和原下载器相同的接口, 并将所有工作委派给原下载器。 不过, 代理类会保存所有的文件下载记录, 如果程序多次请求同一文件, 它会返回缓存的文件。 ``` // 远程服务接口。 interface ThirdPartyTVLib is method listVideos() method getVideoInfo(id) method downloadVideo(id) // 服务连接器的具体实现。该类的方法可以向腾讯视频请求信息。请求速度取决于 // 用户和腾讯视频的互联网连接情况。如果同时发送大量请求,即使所请求的信息 // 一模一样,程序的速度依然会减慢。 class ThirdPartyTVClass implements ThirdPartyTVLib is method listVideos() is // 向腾讯视频发送一个 API 请求。 method getVideoInfo(id) is // 获取某个视频的元数据。 method downloadVideo(id) is // 从腾讯视频下载一个视频文件。 // 为了节省网络带宽,我们可以将请求结果缓存下来并保存一段时间。但你可能无 // 法直接将这些代码放入服务类中。比如该类可能是第三方程序库的一部分或其签 // 名是 `final(最终)`。因此我们会在一个实现了服务类接口的新代理类中放入 // 缓存代码。当代理类接收到真实请求后,才会将其委派给服务对象。 class CachedTVClass implements ThirdPartyTVLib is private field service: ThirdPartyTVLib private field listCache, videoCache field needReset constructor CachedTVClass(service: ThirdPartyTVLib) is this.service = service method listVideos() is if (listCache == null || needReset) listCache = service.listVideos() return listCache method getVideoInfo(id) is if (videoCache == null || needReset) videoCache = service.getVideoInfo(id) return videoCache method downloadVideo(id) is if (!downloadExists(id) || needReset) service.downloadVideo(id) // 之前直接与服务对象交互的 GUI 类不需要改变,前提是它仅通过接口与服务对 // 象交互。我们可以安全地传递一个代理对象来代替真实服务对象,因为它们都实 // 现了相同的接口。 class TVManager is protected field service: ThirdPartyTVLib constructor TVManager(service: ThirdPartyTVLib) is this.service = service method renderVideoPage(id) is info = service.getVideoInfo(id) // 渲染视频页面。 method renderListPanel() is list = service.listVideos() // 渲染视频缩略图列表。 method reactOnUserInput() is renderVideoPage() renderListPanel() // 程序可在运行时对代理进行配置。 class Application is method init() is aTVService = new ThirdPartyTVClass() aTVProxy = new CachedTVClass(aTVService) manager = new TVManager(aTVProxy) manager.reactOnUserInput() ``` ## 应用场景 使用代理模式的方式多种多样, 我们来看看最常见的几种。 - 延迟初始化 (虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。 你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。 - 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。 代理可仅在客户端凭据满足要求时将请求传递给服务对象。 - 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。 在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。 - 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。 - 缓存请求结果 (缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。 代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。 - 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。 代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。 代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。 ## 实现方式 1. 如果没有现成的服务接口, 你就需要创建一个接口来实现代理和服务对象的可交换性。 从服务类中抽取接口并非总是可行的, 因为你需要对服务的所有客户端进行修改, 让它们使用接口。 备选计划是将代理作为服务类的子类, 这样代理就能继承服务的所有接口了。 2. 创建代理类, 其中必须包含一个存储指向服务的引用的成员变量。 通常情况下, 代理负责创建服务并对其整个生命周期进行管理。 在一些特殊情况下, 客户端会通过构造函数将服务传递给代理。 3. 根据需求实现代理方法。 在大部分情况下, 代理在完成一些任务后应将工作委派给服务对象。 4. 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务。 你可以在代理类中创建一个简单的静态方法, 也可以创建一个完整的工厂方法。 5. 可以考虑为服务对象实现延迟初始化。 ## 优点 - 你可以在客户端毫无察觉的情况下控制服务对象。 - 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。 - 即使服务对象还未准备好或不存在, 代理也可以正常工作。 - 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。 ## 缺点 - 代码可能会变得复杂, 因为需要新建许多类。 - 服务响应可能会延迟。 ## 与其他模式的关系 - 适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。 - 外观模式与代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。 - 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。 ## 代码示例 ### TS ``` /** * The Subject interface declares common operations for both RealSubject and the * Proxy. As long as the client works with RealSubject using this interface, * you'll be able to pass it a proxy instead of a real subject. */ interface Subject { request(): void; } /** * The RealSubject contains some core business logic. Usually, RealSubjects are * capable of doing some useful work which may also be very slow or sensitive - * e.g. correcting input data. A Proxy can solve these issues without any * changes to the RealSubject's code. */ class RealSubject implements Subject { public request(): void { console.log('RealSubject: Handling request.'); } } /** * The Proxy has an interface identical to the RealSubject. */ class Proxy implements Subject { private realSubject: RealSubject; /** * The Proxy maintains a reference to an object of the RealSubject class. It * can be either lazy-loaded or passed to the Proxy by the client. */ constructor(realSubject: RealSubject) { this.realSubject = realSubject; } /** * The most common applications of the Proxy pattern are lazy loading, * caching, controlling the access, logging, etc. A Proxy can perform one of * these things and then, depending on the result, pass the execution to the * same method in a linked RealSubject object. */ public request(): void { if (this.checkAccess()) { this.realSubject.request(); this.logAccess(); } } private checkAccess(): boolean { // Some real checks should go here. console.log('Proxy: Checking access prior to firing a real request.'); return true; } private logAccess(): void { console.log('Proxy: Logging the time of request.'); } } /** * The client code is supposed to work with all objects (both subjects and * proxies) via the Subject interface in order to support both real subjects and * proxies. In real life, however, clients mostly work with their real subjects * directly. In this case, to implement the pattern more easily, you can extend * your proxy from the real subject's class. */ function clientCode(subject: Subject) { // ... subject.request(); // ... } console.log('Client: Executing the client code with a real subject:'); const realSubject = new RealSubject(); clientCode(realSubject); console.log(''); console.log('Client: Executing the same client code with a proxy:'); const proxy = new Proxy(realSubject); clientCode(proxy); ``` 输出: ``` Client: Executing the client code with a real subject: RealSubject: Handling request. Client: Executing the same client code with a proxy: Proxy: Checking access prior to firing a real request. RealSubject: Handling request. Proxy: Logging the time of request. ```
追风者
2022年3月29日 10:16
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码