结构型模式-代理模式
结构型模式-代理模式
亦称: Proxy
意图
代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。
问题
为什么要控制对于某个对象的访问呢? 举个例子: 有这样一个消耗大量系统资源的巨型对象, 你只是偶尔需要使用它, 并非总是需要。
你可以实现延迟初始化: 在实际有需要时再创建该对象。 对象的所有客户端都要执行延迟初始代码。 不幸的是, 这很可能会带来很多重复代码。
在理想情况下, 我们希望将代码直接放入对象的类中, 但这并非总是能实现: 比如类可能是第三方封闭库的一部分。
解决方案
代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。
这有什么好处呢? 如果需要在类的主要业务逻辑前后执行一些工作, 你无需修改类就能完成这项工作。 由于代理实现的接口与原类相同, 因此你可将其传递给任何一个使用实际服务对象的客户端。
真实世界类比
信用卡是银行账户的代理, 银行账户则是一大捆现金的代理。 它们都实现了同样的接口, 均可用于进行支付。 消费者会非常满意, 因为不必随身携带大量现金; 商店老板同样会十分高兴, 因为交易收入能以电子化的方式进入商店的银行账户中, 无需担心存款时出现现金丢失或被抢劫的情况。
代理模式结构
- 服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
- 服务 (Service) 类提供了一些实用的业务逻辑。
- 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。
- 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。
伪代码
本例演示如何使用代理模式在第三方腾讯视频 (TencentVideo, 代码示例中记为 TV) 程序库中添加延迟初始化和缓存。
程序库提供了视频下载类。 但是该类的效率非常低。 如果客户端程序多次请求同一视频, 程序库会反复下载该视频, 而不会将首次下载的文件缓存下来复用。
代理类实现和原下载器相同的接口, 并将所有工作委派给原下载器。 不过, 代理类会保存所有的文件下载记录, 如果程序多次请求同一文件, 它会返回缓存的文件。
1 | // 远程服务接口。 |
代理模式适合应用场景
使用代理模式的方式多种多样, 我们来看看最常见的几种。
延迟初始化(虚拟代理)。如果你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。
你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。
访问控制(保护代理)。如果你只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是各种已启动的程序(包括恶意程序),此时可使用代理模式。
代理可仅在客户端凭据满足要求时将请求传递给服务对象。
本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。
在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。
记录日志请求(日志记录代理)。适用于当你需要保存对于服务对象的请求历史记录时。代理可以在向服务传递请求前进行记录。
缓存请求结果 (缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
- 代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。
智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。
代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。
代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。
实现方式
- 如果没有现成的服务接口, 你就需要创建一个接口来实现代理和服务对象的可交换性。 从服务类中抽取接口并非总是可行的, 因为你需要对服务的所有客户端进行修改, 让它们使用接口。 备选计划是将代理作为服务类的子类, 这样代理就能继承服务的所有接口了。
- 创建代理类, 其中必须包含一个存储指向服务的引用的成员变量。 通常情况下, 代理负责创建服务并对其整个生命周期进行管理。 在一些特殊情况下, 客户端会通过构造函数将服务传递给代理。
- 根据需求实现代理方法。 在大部分情况下, 代理在完成一些任务后应将工作委派给服务对象。
- 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务。 你可以在代理类中创建一个简单的静态方法, 也可以创建一个完整的工厂方法。
- 可以考虑为服务对象实现延迟初始化。
代理模式优缺点
优点
- 你可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。
- 即使服务对象还未准备好或不存在, 代理也可以正常工作。
- 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。
缺点
- 代码可能会变得复杂, 因为需要新建许多类。
- 服务响应可能会延迟。
与其他模式的关系
- 适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
- 外观模式与代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
- 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
代码示例
代理是一种结构型设计模式, 让你能提供真实服务对象的替代品给客户端使用。 代理接收客户端的请求并进行一些处理 (访问控制和缓存等), 然后再将请求传递给服务对象。
代理对象拥有和服务对象相同的接口, 这使得当其被传递给客户端时可与真实对象互换。
在 C# 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 C# 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
Program.cs: 概念示例
1 | using System; |
Output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
在 C++ 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 C++ 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
main.cc: 概念示例
1 |
|
Output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
在 Java 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 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: 远程服务接口
1 | package refactoring_guru.proxy.example.some_cool_media_library; |
some_cool_media_library/ThirdPartyYouTubeClass.java: 远程服务实现
1 | package refactoring_guru.proxy.example.some_cool_media_library; |
some_cool_media_library/Video.java: 视频文件
1 | package refactoring_guru.proxy.example.some_cool_media_library; |
proxy
proxy/YouTubeCacheProxy.java: 缓存代理
1 | package refactoring_guru.proxy.example.proxy; |
downloader
downloader/YouTubeDownloader.java: 媒体下载应用
1 | package refactoring_guru.proxy.example.downloader; |
Demo.java: 初始化代码
1 | package refactoring_guru.proxy.example; |
OutputDemo.png: 执行结果
1 | Connecting to http://www.youtube.com... Connected! |
在 PHP 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 PHP 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
1 |
|
Output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
真实世界示例
可供使用的代理类型有无数种: 缓存、 日志、 访问控制和延迟初始化等。 本例演示了代理模式如何通过对结果进行缓存来提高下载器对象的性能。
index.php: 真实世界示例
1 |
|
Output.txt: 执行结果
1 | Executing client code with real subject: |
在 Python 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 Python 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
main.py: 概念示例
1 | from abc import ABC, abstractmethod |
Output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
在 Ruby 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 Ruby 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
main.rb: 概念示例
1 | # The Subject interface declares common operations for both RealSubject and the |
output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
在 Swift 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 Swift 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。
Example.swift: 概念示例
1 | import XCTest |
Output.txt: 执行结果
1 |
真实世界示例
Example.swift: 真实世界示例
1 | import XCTest |
Output.txt: 执行结果
1 | Client: Loading a profile WITHOUT proxy |
在 TypeScript 中使用模式
复杂度: ★★☆
流行度: ★☆☆
使用示例: 尽管代理模式在绝大多数 TypeScript 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。
识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。
概念示例
本例说明了代理设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
index.ts: 概念示例
1 | /** |
Output.txt: 执行结果
1 | Client: Executing the client code with a real subject: |
概念示例
Nginx 这样的 Web 服务器可充当应用程序服务器的代理:
- 提供了对应用程序服务器的受控访问权限。
- 可限制速度。
- 可缓存请求。
server.go: 主体
1 | package main |
nginx.go: 代理
1 | package main |
application.go: 真实主体
1 | package main |
main.go: 客户端代码
1 | package main |
output.txt: 执行结果
1 | Url: /app/status |