设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。本文是第三篇:结构型设计模式
结构型模式主要总结了一些 类或对象组合在一起 的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式,桥接模式,适配器模式,装饰器模式,(2021-12-03) 外观模式(不常用),组合模式(不常用),享元模式(不常用)
结构型设计模式教你如何正确使用继承和组合
定义:为一个对象提供一个替身,以控制对这个对象的访问。 即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
代理模式的使用场景:
①在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志;
②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数据... } }
存在的问题:非当前业务的代码嵌入到了该业务代码中,造成了代码耦合,且职责不单一
如何解决代码耦合问题呢?
方法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
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 来转换字节码并生成新的类
Spring AOP 实现原理?
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP 底层的实现原理就是基于 动态代理。
Aop 动态代理原理图
实现切面的三种方式:
①JDK proxy 如demo2
②Cglib 如demo3
③AspectJ AOP,Spring AOP 已经集成了AspectJ ,底层实现原理为 字节码操作
MyBatis Dao 层实现原理
定义:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
使用场景:对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统
难点:要求正确识别出系统中独立变化的维度(抽象、实现),使用范围有局限性。
Demo1:
继续下面的案例: 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 字段的数据。
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 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。如下图所示:
装饰器模式:主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。能动态地将新功能附加到对象上。
功能增强,也是判断是否该用装饰者模式的一个重要的依据
与代理模式比较
适用场景:当我们需要修改原有功能,但又不愿直接去修改原有代码时,设计一个Decorator 套在原有代码外面。
装饰者模式原理图
装饰者模式的特点总结:
①装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类
②装饰器类的成员变量类型为父类类型
③装饰器类是对功能的增强
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
Demo3 装饰者模式在 Java IO中的使用,Java IO 类库如下图所示:
InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取效率。
现在有一个需求,读取文件 test.txt。
InputStream in = new FileInputStream("/user/1202/test.txt"); InputStream bin = new BufferedInputStream(in); byte[] data = new byte[128]; while (bin.read(data) != -1) { //... }
如果是基于继承的设计,设计成 BufferedFileInputStream,需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
InputStream bin = new BufferedFileInputStream("/user/1202/test.txt"); byte[] data = new byte[128]; while (bin.read(data) != -1) { //... }
基于装饰器模式的设计模式的 Java IO 源码,核心思想:组合优于继承,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。
public abstract class InputStream { //... public int read(byte b[]) throws IOException { return read(b, 0, b.length); } public int read(byte b[], int off, int len) throws IOException { //... } public long skip(long n) throws IOException { //... } public int available() throws IOException { return 0; } public void close() throws IOException {} public synchronized void mark(int readlimit) {} public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } public boolean markSupported() { return false; } } public class BufferedInputStream extends InputStream { protected volatile InputStream in; protected BufferedInputStream(InputStream in) { this.in = in; } //...实现基于缓存的读数据接口... } public class DataInputStream extends InputStream { protected volatile InputStream in; protected DataInputStream(InputStream in) { this.in = in; } //...实现读取基本类型数据的接口 }
概念:适配器模式用来将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,用于消除接口不匹配所造成的类的兼容性问题。
代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。
主要分为两类:
1、类的适配器模式 使用继承关系来实现
2、对象的适配器模式 使用组合关系来实现
Demo1:ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口
// 适配接口 public interface ITarget { void f1(); void f2(); void fc(); } //被适配者 public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } //适配者 继承被适配者,实现适配接口 public class Adaptor extends Adaptee implements ITarget { public void f1() { super.fa(); } public void f2() { //...重新实现f2()... } // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点 } 缺点:需要继承被适配者
// 最终需要的输出 public interface ITarget { void f1(); void f2(); void fc(); } //被适配者 public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } //适配者 组合被适配者,实现适配接口 public class Adaptor implements ITarget { private Adaptee adaptee; public Adaptor(Adaptee adaptee) { this.adaptee = adaptee; } public void f1() { //委托给Adaptee adaptee.fa(); } public void f2() { //...重新实现f2()... } public void fc() { adaptee.fc(); } } 使用组合替代继承,解决了类适配器必须继承 被是适配者的问题
在开发中,到底该如何选择使用哪一种呢?
①一个是 Adaptee 接口的个数
②另一个是 Adaptee 和 ITarget 的契合程度
怎么简便怎么来。相对而言:更加推荐使用对象适配器,因为组合结构相对于继承更加灵活
③ 接口适配
使用场景:
Demo2 修改配置中心SDK
public class InstanceServiceClient { //... public Response<List<InstanceRO>> fetchInstanceByIndustry(InstanceSearchDTO param){ //... } public Response<InstanceRO> fetchInstanceByCondition(SearchCondition param){ //... } public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... } //... } // 使用适配器模式进行重构 public class ITarget { void function1(InstanceSearchDTO param); void function2(SearchCondition param); void fucntion3(ParamsWrapperDefinition paramsWrapper); //... } public class InstanceServiceClientAdaptor extends InstanceServiceClient implements ITarget { //... public void function1() { super.fetchInstanceByIndustry(param); } public void function2() { super.fetchInstanceByCondition(); } public void function3(ParamsWrapperDefinition paramsWrapper) { super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...); } }
Demo3:替换外部系统
// 外部系统A public interface IA { //... void fa(); } public class A implements IA { //... public void fa() { //... } } // 在我们的项目中,外部系统A的使用示例 public class Demo { private IA a; public Demo(IA a) { this.a = a; } //... } Demo d = new Demo(new A()); // 将外部系统A 替换成外部系统B public class BAdaptor implemnts IA { private B b; public BAdaptor(B b) { this.b= b; } public void fa() { //... b.fb(); } } // 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动, // 只需要将BAdaptor如下注入到Demo即可。 Demo d = new Demo(new BAdaptor(new B()));
Demo4 兼容升级 ☆
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且
标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
好处:让项目有个过渡期,而不是强制进行代码修改
场景: 兼容api升级,API治理可以使用这种方案
// Emueration jdk1.0提供 jdk2.0 改为了 iterator public class Collections { public static Emueration emumeration(final Collection c) { return new Enumeration() { Iterator i = c.iterator(); public boolean hasMoreElments() { return i.hashNext(); } public Object nextElement() { return i.next(): } } } }
Demo5:适配器模式在 Java 日志中的应用
Slf4j 这个日志框架,它相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。
它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框
架的接口进行二次封装,适配成统一的 Slf4j 接口定义。
package org.slf4j; public interface Logger { public boolean isTraceEnabled(); public void trace(String msg); public void trace(String format, Object arg); public void trace(String format, Object arg1, Object arg2); public void trace(String format, Object[] argArray); public void trace(String msg, Throwable t); public boolean isDebugEnabled(); public void debug(String msg); public void debug(String format, Object arg); public void debug(String format, Object arg1, Object arg2) public void debug(String format, Object[] argArray) public void debug(String msg, Throwable t); //...省略info、warn、error等一堆接口 } // log4j日志框架的适配器 // Log4jLoggerAdapter实现了LocationAwareLogger接口, // 其中LocationAwareLogger继承自Logger接口, // 也就相当于Log4jLoggerAdapter实现了Logger接口。 package org.slf4j.impl; public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable { final transient org.apache.log4j.Logger logger; // log4j public boolean isDebugEnabled() { return logger.isDebugEnabled(); } public void debug(String msg) { logger.log(FQCN, Level.DEBUG, msg, null); } public void debug(String format, Object arg) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String format, Object arg1, Object arg2) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg1, arg2); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String format, Object[] argArray) { if (logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); } } public void debug(String msg, Throwable t) { logger.log(FQCN, Level.DEBUG, msg, t); } //...省略一堆接口的实现... }
Demo6 适配器模式在 SpringMVC 框架应用的源码剖析
SpringMVC 处理用户请求的流程
SpringMVC 中的 HandlerAdapter, 就使用了适配器模式
//多种Controller实现 public interface Controller { } class HttpController implements Controller { public void doHttpHandler() { System.out.println("http..."); } } class SimpleController implements Controller { public void doSimpleHandler() { System.out.println("simple..."); } } class AnnotationController implements Controller { public void doAnnotationHandler() { System.out.println("annotation..."); } } public class DispatchServlet extends FrameworkServlet { public static List<HandlerAdapter> handlerAdapters = new ArrayList<HandlerAdapter>(); public DispatchServlet() { handlerAdapters.add(new AnnotationHandlerAdapter()); handlerAdapters.add(new HttpHandlerAdapter()); handlerAdapters.add(new SimpleHandlerAdapter()); } public void doDispatch() { // 此处模拟 SpringMVC 从 request 取 handler 的对象,适配器可以获取到想要的Controller HttpController controller = new HttpController(); //AnnotationController controller = new AnnotationController(); //SimpleController controller = new SimpleController(); // 得到对应适配器 HandlerAdapter adapter = getHandler(controller); // 通过适配器执行对应的controller对应方法 adapter.handle(controller); } public HandlerAdapter getHandler(Controller controller) { //遍历:根据得到的controller(handler), 返回对应适配器 for (HandlerAdapter adapter : this.handlerAdapters) { if (adapter.supports(controller)) { return adapter; } } return null; } public static void main(String[] args) { new DispatchServlet().doDispatch(); // http... } } ///定义一个Adapter接口 public interface HandlerAdapter { public boolean supports(Object handler); public void handle(Object handler); } // 多种适配器类 class SimpleHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof SimpleController); } public void handle(Object handler) { ((SimpleController) handler).doSimplerHandler(); } } class HttpHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof HttpController); } public void handle(Object handler) { ((HttpController) handler).doHttpHandler(); } } class AnnotationHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof AnnotationController); } public void handle(Object handler) { ((AnnotationController) handler).doAnnotationHandler(); } }
使用HandlerAdapter 的原因:
可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用Controller方法,需要调用的时候就得不断是使用if else来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了OCP原则。
SpringMVC使用适配器设计模式的好处:
• Spring 定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类;
• 适配器代替 Controller 执行相应的方法;
• 扩展 Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,控制访问,而非加强功能
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
适配器模式:一种补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
20211203 done
定义:门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用,合理的使用门面模式,可以帮我们更好的划分访问层次。
使用场景:
① 解决易用性问题 封装底层的复杂实现;
② 解决性能问题 将多个接口组装为一个大而全接口,减少网络开销;
③维护遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互, 提高复用性。
门面模式在商品中心使用广泛,但是有些 facade 类并不是出于封装底层子系统复杂实现的目的编写的,这些业务逻辑不复杂的接口去除 facade 后缀为好。
Demo1 facade模式的使用姿势
例如:业务方需要在一个接口中返回商品信息(配置项、类目、属性、价格、库存)
public class ItemReadFacade { //定义各个子系统对象 private ItemConfig itemConfig; private Category category; private Attributes attributes; private Price price; private Stock stock; //构造器 public ItemReadFacade() { super(); this.itemConfig = ItemConfig.getInstance(); this.category = Category.getInstance(); this.attributes = Attributes.getInstance(); this.price = Price.getInstance(); this.stock = Stock.getInstance(); } //操作分成 4 步 public ItemWrapperDTO read() { itemConfig.read(); category.read(); attributes.read(); price.read(); stock.read(); } } public class ItemConfig { //使用单例模式, 使用饿汉式 private static ItemConfig instance = new ItemConfig(); public static ItemConfig getInstanc() { return instance; } public ItemConfig read() { System.out.println(" ItemConfig "); } } ... public static void main(String[] args) { ItemReadFacade itemReadFacade = new ItemReadFacade(); itemReadFacade.read(); }
Demo2:门面模式在 MyBatis 框架应用的分析
MyBatis 中的Configuration 去创建MetaObject 对象使用到门面模式
public class Configuration { protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory(); protected ObjectFactory objectFactory = new DefaultObjectFactory(); protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory(); public MetaObject newMetaObject(Object object) { // 封装了子系统的复杂性 return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory); } } public class MetaObject{ private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { this.originalObject = object; this.objectFactory = objectFactory; this.objectWrapperFactory = objectWrapperFactory; this.reflectorFactory = reflectorFactory; if (object instanceof ObjectWrapper) { this.objectWrapper = (ObjectWrapper)object; } else if (objectWrapperFactory.hasWrapperFor(object)) { this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); } else if (object instanceof Map) { this.objectWrapper = new MapWrapper(this, (Map)object); } else if (object instanceof Collection) { this.objectWrapper = new CollectionWrapper(this, (Collection)object); } else { this.objectWrapper = new BeanWrapper(this, object); } } public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { return object == null ? SystemMetaObject.NULL_META_OBJECT : new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory); } }
概念:将对象组合成树状结构以表示 “整体-部分”的层次关系。
使用场景:用来处理树状结构数据。
组合模式的角色及职责
Demo1 需求:设计一个“类目”类,能方便地实现下面这些功能:
代码如下所示,把父类目和子类目统一用 CategoryNode 类来表示,并通过hasChildren 属性来区分父子节点。
public class CategoryNode { //类目名称 private String name; //是否有叶子节点 private boolean hasChildren; //子节点 private List<CategoryNode> subNodes = new ArrayList<>(); public CategoryNode(String name, boolean hasChildren) { this.name = name; this.hasChildren = hasChildren; } public int countNumOfCategories() { if (!hasChildren) { return 1; } int numOfCategories = 0; for (CategoryNode categoryNode : subNodes) { numOfCategories += categoryNode.countNumOfCategories(); } } public String getName() { return name; } public void addSubNode(CategoryNode categoryNode) { subNodes.add(categoryNode); } public void removeSubNode(CategoryNode categoryNode) { int size = subNodes.size(); int i = 0; for (; i < size; ++i) { if (subNodes.get(i).getName().equals(categoryNode.getName())){ break; } } if (i < size) { subNodes.remove(i); } } } public class Demo { public static void main(String[] args) { CategoryNode pCategoryNode = new CategoryNode("名贵花木"); CategoryNode node_1 = new CategoryNode("笔记本电脑"); CategoryNode node_2 = new CategoryNode("台式整机"); pCategoryNode.addSubNode(node_1); pCategoryNode.addSubNode(node_2); System.out.println("category num:" + pCategoryNode.countNumOfCategories()); } }
Demo2 构建整个公司的人员架构图(部门、子部门、员工的隶属关系),提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和),部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。从这个角度来看,该场景可以使用组合模式来设计和实现。
HumanResource 是部门类(Department)和员工类(Employee)抽象出来的父类,提供统一薪资的处理逻辑。
public abstract class HumanResource { protected long id; protected double salary; public HumanResource(long id) { this.id = id; } public long getId() { return id; } public abstract double calculateSalary(); } // 员工 public class Employee extends HumanResource { public Employee(long id, double salary) { super(id); this.salary = salary; } @Override public double calculateSalary() { return salary; } } // 部门 public class Department extends HumanResource { private List<HumanResource> subNodes = new ArrayList<>(); public Department(long id) { super(id); } @Override public double calculateSalary() { double totalSalary = 0; for (HumanResource hr : subNodes) { totalSalary += hr.calculateSalary(); } this.salary = totalSalary; return totalSalary; } } public void addSubNode(HumanResource hr) { subNodes.add(hr); } } // 构建组织架构的代码 public class Demo { private static final long ORGANIZATION_ROOT_ID = 1001; // 依赖注入 private DepartmentRepo departmentRepo; // 依赖注入 private EmployeeRepo employeeRepo; public void buildOrganization() { Department rootDepartment = new Department(ORGANIZATION_ROOT_ID); buildOrganization(rootDepartment); } private void buildOrganization(Department department) { List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department); for (Long subDepartmentId : subDepartmentIds) { Department subDepartment = new Department(subDepartmentId); department.addSubNode(subDepartment); buildOrganization(subDepartment); } List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId()); for (Long employeeId : employeeIds) { double salary = employeeRepo.getEmployeeSalary(employeeId); department.addSubNode(new Employee(employeeId, salary)); } } }
Demo3 组合模式在 HashMap 的源码分析
职责:
①Map起 Component 的作用,提供通用的能力;
②Node 起 leaf 的作用,在组合中表示叶子节点;
②HashMap起 Composite 的作用,继承 Map接口,借助 Node 实现 Map 的功能
public interface Map<K,V> { V put(K key, V value); V remove(Object key); ... } public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } // 调用Node节点的put实现功能 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } } static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
定义:运用共享技术有效地支持大量细粒度的对象。
享元模式可以分为以下两种角色
使用场景:各类池技术:String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
享元模式的缺点:享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此不要过度使用这个模式。
Demo1:棋牌游戏,一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。
我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。
// 享元角色 public class ChessPieceUnit { private int id; private String text; private Color color; public ChessPieceUnit(int id, String text, Color color) { this.id = id; this.text = text; this.color = color; } public static enum Color { RED, BLACK } // ...省略其他属性和getter方法... } // 享元工厂,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的 public class ChessPieceUnitFactory { private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>(); static { pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK)); pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK)); //...省略摆放其他棋子的代码... } public static ChessPieceUnit getChessPiece(int chessPieceId) { return pieces.get(chessPieceId); } } // 不可共享的角色 public class ChessPiece { private ChessPieceUnit chessPieceUnit; private int positionX; private int positionY; public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) { this.chessPieceUnit = unit; this.positionX = positionX; this.positionY = positionY; } // 省略getter、setter方法 } // Client public class ChessBoard { private Map<Integer, ChessPiece> chessPieces = new HashMap<>(); public ChessBoard() { init(); } private void init() { chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(1), 0,0)); chessPieces.put(1, new ChessPiece( ChessPieceUnitFactory.getChessPiece(2), 1,0)); //...省略摆放其他棋子的代码... } public void move(int chessPieceId, int toPositionX, int toPositionY) { //...省略... } }
Demo2 剖析一下享元模式在 Java Integer、String 中的应用。
通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象时,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。
Integer i1 = 56; //自动装箱 Integer i = Integer.valueOf(59); Integer i2 = 56; Integer i3 = 129; Integer i4 = 129; System.out.println(i1 == i2); // true System.out.println(i3 == i4); //false public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high") if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
可以用如下方式,将缓存的最大值从 127 调整到 255
//方法一: -Djava.lang.Integer.IntegerCache.high=255 //方法二: -XX:AutoBoxCacheMax=255
使用建议:在日常开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种
Integer a = new Integer(123); // 并不会使用到 IntegerCache Integer a = 123; Integer a = Integer.valueOf(123);
享元模式在 Java String 中的应用
JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。对于字符串来说,没法事先知道要共享哪些字符串常量,只能在某个字符串常量第一次被用到时,存储到常量池中,当之后再用到时,直接引用常量池中已经存在的即可,就不需要再重新创建了。
String s1 = "设计模式"; String s2 = "设计模式"; String s3 = new String("设计模式"); System.out.println(s1 == s2); //true System.out.println(s1 == s3); //false