9、结构型设计模式
结构型模式主要总结了一些 类或对象组合在一起 的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式,桥接模式,适配器模式,装饰器模式,(2021-12-03) 外观模式(不常用),组合模式(不常用),享元模式(不常用)
结构型设计模式教你如何正确使用继承和组合
9.1、代理模式 Proxy
9.1.1、代理模式定义
- 为一个对象提供一个替身,以控制对这个对象的访问。 即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。**
- 代理模式的使用场景:
- ①在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志;
- ②RPC 框架也可以看作一种代理模式。
- 如何使用:
- 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能。①如果有接口,让代理类和原始类实现同样的接口(JDK 代理);②如果原始类并没有定义接口,我们可以通过让代理类继承原始类的方法来实现代理模式(Cglib 代理)。
Demo 用户登录业务
public class UserController { //...省略其他属性和方法... @Autowired 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, startTimes); metricsCollector.recordRequest(requestInfo); //...返回UserVo数据... } }
存在的问题:非当前业务的代码嵌入到了该业务代码中,造成了代码耦合,且职责不单一
9.1.2、如何解决代码耦合问题呢?
方法1、静态代理:在使用时需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
Demo 静态代理
// 接口 public interface IUserController { void login(String telephone, String password); } // 实现类 1 public class UserController implements IUserController { @Override public void login(String telephone, String password) { // 业务逻辑 } } // 实现类2 代理对象,静态代理 public class UserControllerProxy implements IUserController{ // 将目标对象组合到代理类中 private IUserController userController; private MetricsCollector metricsCollector; //构造器 public UserControllerProxy(IUserController userController, MetricsCollector metricsCollector) { this.userController = userController; this.metricsCollector = metricsCollector; } @Override public void login() { //方法 System.out.println("开始代理 完成某些操作。。。。。 "); userController.login(); //方法 System.out.println("提交。。。。。"); metricsCollector.recordRequest(); } } public static void main(String[] args) { //被代理对象 UserController userController = new UserController(); //创建代理对象, 同时将被代理对象传递给代理对象 UserControllerProxy userControllerProxy = new UserControllerProxy(userController); //执行的是代理对象的方法,代理对象再去调用被代理对象的方法 userControllerProxy.login(); } 优点:在不修改目标对象代码前提下, 能通过代理对象对目标功能扩展 缺点:代理对象需要与目标对象实现同样的接口,所以有很多代理类,维护很困难
如何解决代理类过多的问题?
动态代理:我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
分为两种:
①JDK 动态代理,目标对象需要实现接口
② Cglib 动态代理,目标对象不需要实现对象
Demo2 JDK 动态代理 MetricsCollectorProxy 作为一个动态代理类,动态地给每个需要收集接口请求信息的类创建代理类
// JDK 动态代理 底层依赖 Java 反射语法 public class ProxyFactory { // 被代理的对象 private Object target; // 在构造器中对目标对象进行初始化 public ProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { Class<?>[] interfaces = target.getClass().getInterfaces(); DynamicProxyHandler handler = new DynamicProxyHandler(target); // 由 JDK 提供核心接口 // 1、ClassLoader loader:指定当前被代理对象的类加载器 // 2、Class<?> interfaces: 被代理对象实现的接口类型,使用泛型方法确认类型 // 3、InvocationHandler h 事件处理,执行被代理对象的方法时,会去触发事件处理器方法,会把当前执行的被代理对象方法作为参数 return Proxy.newProxyInstance(target.getClass().getClassLoader(), interfaces, handler); } private class DynamicProxyHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 前置处理。。。 System.out.println("jdk 代理模式--前置处理"); // 使用反射机制调用目标对象的方法 Object result = method.invoke(target, args); // 后置处理。.. System.out.println("jdk 代理模式--后置处理"); return result; } } } public interface MetricsCollector { /* 数据统计 */ String recordRequest(RequestInfoVo vo); } public class MetricsCollectorImpl implements MetricsCollector { /* 数据统计 */ @Override public String recordRequest(RequestInfoVo vo) { return "数据统计"; } } public class Client { public static void main(String[] args) { // 创建目标对象 MetricsCollectorImpl target = new MetricsCollectorImpl(); // 获取到代理对象,并将目标对象传递给代理对象 MetricsCollector proxyInstance = (MetricsCollector) new ProxyFactory(target).getProxyInstance(); // 执行代理对象的方法,触发 intercept 方法 String res = proxyInstance.recordRequest(new RequestInfoVo()); System.out.println("res:" + res); } } // 返回的数据:当前代理动态生成的对象,如果调用该对象的 getClass() 方法,返回的是$Proxy0
9.1.3、Cglib 动态代理
目标对象只有一个单独的对象,并没有实现任何的接口,这是使用被代理对象子类来实现代理。
Demo3 Cglib 动态代理
1、需要引入 cglib 的 jar 包
2、注意代理的类不能为 final,否则报错 java.lang.illegalArgumentException;目标对象的方法方法如果是 final/static, 那么就不会被拦截,即不会执行目标对象的额外业务方法
3、使用示例代码如下所示
public class ProxyFactory implements MethodInterceptor { // 维护目标对象 private Object target; // 在构造器中对目标对象进行初始化 public ProxyFactory(Object target) { this.target = target; } // 返回代理对象,是 target 的对象的代理对象 public Object getProxyInstance() { // 1、设置工具类 Enhancer enhancer = new Enhancer(); // 2、设置父类 enhancer.setSuperclass(target.getClass()); // 3、设置回调函数 enhancer.setCallback(this); // 4、创建子类对象,即代理对象 return enhancer.create(); } // 重写 intercept 方法,会调用目标对象的方法 @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("Cglib 代理模式--前置处理"); Object invoke = method.invoke(target, args); System.out.println("Cglib 代理模式--后置处理"); return invoke; } } public class MetricsCollector { public String stastic() { return "统计信息~"; } } public class Client { public static void main(String[] args) { // 创建目标对象 MetricsCollector target = new MetricsCollector(); // 获取到代理对象,并将目标对象传递给代理对象 MetricsCollector proxyInstance = (MetricsCollector) new ProxyFactory(target).getProxyInstance(); // 执行代理对象的方法,触发 intercept 方法 String res = proxyInstance.stastic(); System.out.println("res:" + res); } }
在Aop 编程中如何选择代理模式?
①目标对象需要实现接口,使用 JDK 代理
②目标对象不需要实现接口,使用 Cglib 代理
底层原理:使用字节码处理框架 ASM 来转换字节码并生成新的类
9.1.4、Spring AOP 实现原理?
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP 底层的实现原理就是基于 动态代理。
Aop 动态代理原理图
实现切面的三种方式:
①JDK proxy 如demo2
②Cglib 如demo3
③AspectJ AOP,Spring AOP 已经集成了AspectJ ,底层实现原理为 字节码操作
- 使用方式,可以看这篇文章
- AspectJ 切面注解中五种通知注解:@Before、@After、@AfterRunning、@AfterThrowing、@Around
- 商品中心代码中大量应用了Spring AOP 处理非业务逻辑
MyBatis Dao 层实现原理
- 使用到了代理模式
- 后续补充原理
9.2、桥接模式(不常用)
9.2.1、桥接模式定义
- 将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
使用场景:对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统
- 1、JDBC驱动程序
- Driver(接口):mysql驱动、oracle驱动
- JDBC(抽象):JDBC这套类库
- 2、银行转账系统
- 转账分类(接口):网上转账,柜台,ATM机器
- 用户类型(抽象):普通用户,会员用户
- 3、消息管理
- 消息类型(接口):严重、紧急、普通
- 消息分类(抽象):邮件、短信、微信、手机
难点:要求正确识别出系统中独立变化的维度(抽象、实现),使用范围有局限性。
Demo1 API 接口监控告警的代码
继续下面的案例:
- API 接口监控告警的代码。根据不同的告警规则,触发不同类型的告警。
Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。
首先看看最简单的实现方式:
public enum NotificationEmergencyLevel { SEVERE, URGENCY, NORMAL, TRIVIAL } public class Notification { private List<String> emailAddresses; private List<String> telephones; private List<String> wechatIds; public Notification() {} public void setEmailAddress(List<String> emailAddress) { this.emailAddresses = emailAddress; } public void setTelephones(List<String> telephones) { this.telephones = telephones; } public void setWechatIds(List<String> wechatIds) { this.wechatIds = wechatIds; } public void notify(NotificationEmergencyLevel level, String message) { if (level.equals(NotificationEmergencyLevel.SEVERE)) { //...自动语音电话 } else if (level.equals(NotificationEmergencyLevel.URGENCY)) { //...发微信 } else if (level.equals(NotificationEmergencyLevel.NORMAL)) { //...发邮件 } else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) { //...发邮件 } } } //在API监控告警的例子中,我们如下方式来使用Notification类: public class ErrorAlertHandler extends AlertHandler { public ErrorAlertHandler(AlertRule rule, Notification notification){ super(rule, notification); } @Override public void check(ApiStatInfo apiStatInfo) { if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()) notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
通知类中 if-else逻辑比较复杂,所有发送通知的逻辑都堆砌在Notification类中,如果将发送消息的逻辑剥离出来,形成独立的消息发送类 MsgSender,即 Notification相当于抽象,MsgSender相当于实现,两者可以独立开发,通过组合关系 任意组合在一起。
消息类型(MsgSender 接口):严重、紧急、普通
消息分类(Notification 抽象类):邮件、短信、微信、手机
重构后的代码如下所示:
public interface MsgSender { void send(String message); } public class TelephoneMsgSender implements MsgSender { private List<String> telephones; public TelephoneMsgSender(List<String> telephones) { this.telephones = telephones; } @Override public void send(String message) { //... } } public class EmailMsgSender implements MsgSender { // 与TelephoneMsgSender代码结构类似,所以省略... } public class WechatMsgSender implements MsgSender { // 与TelephoneMsgSender代码结构类似,所以省略... } public abstract class Notification { protected MsgSender msgSender; public Notification(MsgSender msgSender) { this.msgSender = msgSender; } public abstract void notify(String message); } public class SevereNotification extends Notification { public SevereNotification(MsgSender msgSender) { super(msgSender); } @Override public void notify(String message) { msgSender.send(message); } } public class UrgencyNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class NormalNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public class TrivialNotification extends Notification { // 与SevereNotification代码结构类似,所以省略... } public static void main(String[] args) { //通过电话发送紧急通知 Notification notifi1 = new UrgencyNotification(new TelephoneMsgSender()); notifi1.notify("电话通知紧急消息"); System.out.println("======================="); //通过邮件发送普通消息 Notification notifi2 = new NormalNotification(new EmailMsgSender()); notifi1.notify("邮件通知普通消息"); }
Demo2 桥接模式在 JDBC 的源码剖析
JDBC 驱动是桥接模式的经典应用,利用 JDBC 驱动来查询数据库。具体的代码如下所示
//加载及注册JDBC驱动程序,切换 oracle 使用 "oracle.jdbc.driver.OracleDriver" Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=root Connection con = DriverManager.getConnection(url); Statement stmt = con.createStatement(); String query = "select * from test"; ResultSet rs=stmt.executeQuery(query); while(rs.next()) { rs.getString(1); rs.getInt(2); }
如果想将 mysql 数据库替换为 Oracle数据库,只需要修改 url 字段的数据。
Demo3 JDBC 是如何优雅的切换数据库呢?
当执行 Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,实际上是做了两件事情。
- ①要求 JVM 查找并加载指定的 Driver 类,
- ②执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager 类中。
package com.mysql.jdbc; import java.sql.SQLException; public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { // 将mysql driver 注册到 DriverManager 中 DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } /** * Construct a new driver and register it with DriverManager * @throws SQLException if a database error occurs. */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
DriverManager 类是干什么用的。具体的代码如下所示。Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因。
//并不是一个抽象类 public class DriverManager { private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); //... static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } //... public static synchronized void registerDriver(java.sql.Driver driver) throws SQLExcepton { if (driver != null) { registeredDrivers.addIfAbsent(new DriverInfo(driver)); } else { throw new NullPointerException(); } } public static Connection getConnection(String url, String user, String password) { java.util.Properties info = new java.util.Properties(); if (user != null) { info.put("user", user); } if (password != null) { info.put("password", password); } return (getConnection(url, info, Reflection.getCallerClass())); } //... }
JDBC中,什么是抽象?
- JDBC 本身就相当于“抽象”,并非指“抽象类"
- 与具体数据库无关的、被抽象出来的一套“类库”
什么是实现?
- 具体的Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”
- 并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”
JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。如下图所示:
9.3、装饰器模式 Decorator(BufferedInputStream)
9.3.1、装饰器模式定义
- 主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类增强功能。能动态地将新功能附加到对象上。
功能增强,也是判断是否该用装饰者模式的一个重要的依据。类比生活中的场景:创建一个对象 “xxx”,给对象添加不同的装饰,穿上夹克、戴上帽子…,这个执行过程就是装饰者模式
与代理模式比较
- 代理类附加的是跟原始类无关的功能,
- 而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
适用场景:当我们需要修改原有功能,但又不愿直接去修改原有代码时,设计一个Decorator 套在原有代码外面。
9.3.2、装饰者模式原理图
9.3.3、装饰者模式的特点总结
①装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类
②装饰器类的成员变量类型为父类类型
③装饰器类是对功能的增强
Demo1 使用示例
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类) public interface IA { void f(); } public class A impelements IA { public void f() { //... } } public class ADecorator impements IA { private IA a; public ADecorator(IA a) { this.a = a; } public void f() { // 功能增强代码 a.f(); // 功能增强代码 } }
Demo2 装饰者模式在商品中心的应用- 类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责。
例如:is里面的
ip里面的 NewFullItemWrapper
im里面的 MqMessageWrapperDto