本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是代理模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的角色定义及调用关系以及其模版代码
- 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构
模式档案
在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,买房不需要直接找业主谈,找房产中介谈即可(贝壳打钱),再比如下载Docker的镜像由于网速限制不直接从DockerHub上下载,直接从阿里云镜像加速站点下载(阿里打钱)。在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间;因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等
模式定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介
模式特点:主要特点是能最大限度的保护目标对象或扩展目标对象的功能,以及解耦目标对象与访问对象。
解决什么问题:主要解决在软件系统中直接访问目标对象时带来的问题。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层
优点:该模式的主要优点如下:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将访问对象与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
缺点:该模式的主要缺点如下:
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
- 增加了系统的复杂度
使用场景: 该模式通常适用于以下场景。
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。代理模式在平时的开发经常被用到,常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。
模式结构
代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构和实现方法,主要有以下三个角色:
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
角色的相互调用关系如下图所示:
在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。根据代理的创建时期,代理模式分为静态代理和动态代理。
- 静态代理:创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
- 动态代理:在程序运行时,运用反射机制动态创建而成
关于静态代理和动态代理在我的这篇Blog中详细介绍过:【Spring学习笔记 六】静态/动态代理实现机制
模式实现
下面通过代码结构来看下如何实现一个代理模式:
1 抽象主题
//抽象主题 interface Subject { void request(); }
2 真实主题
//真实主题 class RealSubject implements Subject { public void request() { System.out.println("访问真实主题!"); } }
3 代理类
//代理对象 @AllArgsConstructor class Proxy implements Subject { private RealSubject realSubject; public void request() { preRequest(); realSubject.request(); postRequest(); } public void preRequest() { System.out.println("访问真实主题之前的预处理!"); } public void postRequest() { System.out.println("访问真实主题之后的后续处理!"); } }
客户端调用如下
public class ProxyTest { public static void main(String[] args) { RealSubject realSubject = new RealSubject(); Proxy proxy = new Proxy(realSubject); //客户端不直接访问对象的方法,而是通过代理对象访问,并在增加了附加的处理逻辑 proxy.request(); } }
打印结果如下:
模式实践
我们来看两个代理模式的实践:设计一个数据库连接辅助工具 以及 设计一个性能计数器
设计一个数据库连接辅助工具
我们想要在创建MySQL数据库连接时做如下两个动作:在连接前判断当前登录人是否有权限创建连接,在获取连接后加一个日志,输出确实创建连接成功了。
package com.example.designpattern.proxy; public class DbConnect { public static void main(String[] args) { MySqlConnectionCreateImpl mySqlConnectionCreateImpl = new MySqlConnectionCreateImpl(); SqlConnectionProxy proxy = new SqlConnectionProxy(mySqlConnectionCreateImpl); proxy.createMySqlConnection("TML"); } } interface MySqlConnectionCreate { void createMySqlConnection(String dbName); } class MySqlConnectionCreateImpl implements MySqlConnectionCreate { @Override public void createMySqlConnection(String dbName) { System.out.println("创建MySQL数据库连接成功,连接到:" + dbName); } } class SqlConnectionProxy implements MySqlConnectionCreate { private MySqlConnectionCreateImpl mySqlConnectionCreateImpl; public SqlConnectionProxy(MySqlConnectionCreateImpl mySqlConnectionCreateImpl) { this.mySqlConnectionCreateImpl = mySqlConnectionCreateImpl; } @Override public void createMySqlConnection(String dbName) { this.doSomethingBefore(); mySqlConnectionCreateImpl.createMySqlConnection(dbName); this.doSomethingAfter(); } private void doSomethingBefore() { System.out.println("连接数据库前的额外操作:权限验证"); } private void doSomethingAfter() { System.out.println("连接数据库前的额外操作:日志记录"); } }
调用结果如下:
设计一个性能计数器
假设我们有一个MetricsCollector 类,用来收集接口请求的原始数据,比如访问时间、处理时长等。我们最直接的想法就是:
public class UserController { //...省略其他属性和方法... private MetricsCollector metricsCollector; // 依赖注入 public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // ... 省略login逻辑... long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); //...返回UserVo数据... } public UserVo register(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // ... 省略register逻辑... long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); //...返回UserVo数据... } }
上面的写法有两个问题。第一,MetricsCollector 代码侵入到业务代码中,跟业务代码高度耦合。如果未来需要替换这个框架,那替换的成本会比较大。第二,收集接口请求的代码跟业务代码无关,本就不应该放到一个类中。业务类最好职责更加单一,只聚焦业务处理。于是我们用代理来解决这个问题。
1 代理类分离业务代码和性能计数器
为了将框架代码和业务代码解耦,代理模式就派上用场了。代理类 UserControllerProxy 和原始类 UserController 实现相同的接口 IUserController。UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码
抽象主题
public interface IUserController { UserVo login(String telephone, String password); UserVo register(String telephone, String password); }
真实主题
public class UserController implements IUserController { //...省略其他属性和方法... @Override public UserVo login(String telephone, String password) { //...省略login逻辑... //...返回UserVo数据... } @Override public UserVo register(String telephone, String password) { //...省略register逻辑... //...返回UserVo数据... } }
代理类
public class UserControllerProxy implements IUserController { private MetricsCollector metricsCollector; private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; this.metricsCollector = new MetricsCollector(); } @Override public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); // 委托 UserVo userVo = userController.login(telephone, password); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return userVo; } @Override public UserVo register(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); UserVo userVo = userController.register(telephone, password); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return userVo; } }
客户端调用
//UserControllerProxy使用举例 //因为原始类和代理类实现相同的接口,是基于接口而非实现编程 //将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码 IUserController userControllerProxy = new UserControllerProxy(new UserController()); userControllerProxy.login("188****6234","tttttt");
以上代码还有两个问题:
- 一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。
- 另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。如果有 50 个要添加附加功能的原始类,那我们就要创建 50 个对应的代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。
那么该怎么处理这个问题呢?
2 动态代理降低代码维护成本
那这个问题怎么解决呢?我们可以使用动态代理来解决这个问题。所谓动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。那如何实现动态代理呢?
代理工厂
public class MetricsCollectorProxyFactory { private MetricsCollector metricsCollector; public MetricsCollectorProxyFactory () { this.metricsCollector = new MetricsCollector(); } public Object createProxy(Object proxiedObject) { Class<?>[] interfaces = proxiedObject.getClass().getInterfaces(); DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject); return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler); } private class DynamicProxyHandler implements InvocationHandler { private Object proxiedObject; public DynamicProxyHandler(Object proxiedObject) { this.proxiedObject = proxiedObject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long startTimestamp = System.currentTimeMillis(); Object result = method.invoke(proxiedObject, args); long endTimeStamp = System.currentTimeMillis(); long responseTime = endTimeStamp - startTimestamp; String apiName = proxiedObject.getClass().getName() + ":" + method.getName(); RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp); metricsCollector.recordRequest(requestInfo); return result; } } }
客户端调用
//MetricsCollectorProxy使用举例,当需要为IUserController接口的UserController实现类进行性能计数器代理时,只需为UserController创建一个userControllerProxy代理类即可 MetricsCollectorProxyFactory proxyFactory = new MetricsCollectorProxyFactory (); //通过MetricsCollectorProxyFactory 代理工厂创建一个IUserController业务的代理,包含性能计数器的实现 IUserController userControllerProxy = (IUserController) proxyFactory.createProxy(new UserController()); userControllerProxy.login("188****6234","tttttt");
模式扩展
上述内容只是代理模式的基本定义方式,也就是我们通常说的静态代理模式,动态代理模式更灵活,不违反开闭原则,实际软件工程中应用的很多都是动态代理,之前在学习各种框架时实际上已经简单探索过:
- 【MyBatis学习笔记 四】MyBatis基本运行原理源码解析,关于动态代理技术,无实现类和有实现类的动态代理,MyBatis就是使用了无实现类的动态代理来工作的。
- 【Spring学习笔记 六】静态/动态代理实现机制,关于AOP的底层实现机制,动态代理与静态代理的区别与联系
- 【基于HTTP的远程调用框架 一】深度详解Retrofit2框架概念和使用:使用代理模式将注解和接口构造成真实的Http请求实现一层封装,使调用者不用关心Http的构造过程,使得调用Http请求看起来就像调用本地接口一样。
下图是动态代理的实现方式
总结一下
其实代理模式之前在项目中用到很多,包括开源框架原理学习(mybatis和spring aop)、远程proxy调用(retrofit2)等,只是当时对这些实际应用没有一个准确的归纳,现在学习完后才知道其实之前用到的各种代理其设计思想都是来源于这种设计模式。所以说设计模式其实是一种思想,掌握了思想就能更好的理解思想在实际场景中的各种应用,不变应万变。