9、结构型设计模式
结构型模式主要总结了一些 类或对象组合在一起 的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式,桥接模式,适配器模式,装饰器模式,(2021-12-03) 外观模式(不常用),组合模式(不常用),享元模式(不常用)
结构型设计模式教你如何正确使用继承和组合
9.1、代理模式 Proxy
定义:为一个对象提供一个替身,以控制对这个对象的访问。 即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
代理模式的使用场景:
①在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志;
②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 ,底层实现原理为 字节码操作
- 使用方式,可以看这篇文章
- AspectJ 切面注解中五种通知注解:@Before、@After、@AfterRunning、@AfterThrowing、@Around
- 商品中心代码中大量应用了Spring AOP 处理非业务逻辑
MyBatis Dao 层实现原理
- 使用到了代理模式
- 后续补充原理
9.2、桥接模式(不常用)
定义:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
使用场景:对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统
- 1、JDBC驱动程序
- Driver(接口):mysql驱动、oracle驱动
- JDBC(抽象):JDBC这套类库
- 2、银行转账系统
- 转账分类(接口):网上转账,柜台,ATM机器
- 用户类型(抽象):普通用户,会员用户
- 3、消息管理
- 消息类型(接口):严重、紧急、普通
- 消息分类(抽象):邮件、短信、微信、手机
难点:要求正确识别出系统中独立变化的维度(抽象、实现),使用范围有局限性。
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(比如,com.mysql.jdbc.Driver)就相当于“实现”
- 并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”
JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。如下图所示:
9.3、装饰者模式 Decorator(BufferedInputStream)
装饰器模式:主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。能动态地将新功能附加到对象上。
功能增强,也是判断是否该用装饰者模式的一个重要的依据
与代理模式比较
- 代理类附加的是跟原始类无关的功能,
- 而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
适用场景:当我们需要修改原有功能,但又不愿直接去修改原有代码时,设计一个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; } //...实现读取基本类型数据的接口 }
9.4、适配器设计模式 Adapter
概念:适配器模式用来将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作,用于消除接口不匹配所造成的类的兼容性问题。
代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。
主要分为两类:
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的扩展了
代理、桥接、装饰器、适配器 4 种设计模式的区别
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,控制访问,而非加强功能
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
适配器模式:一种补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
20211203 done
9.5、你在编码时最常用的设计模式有哪些?在什么场景下用?在业务代码中,经常发现大量XXFacade,门面模式是解决什么问题?适用于什么场景?
定义:门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用,合理的使用门面模式,可以帮我们更好的划分访问层次。
使用场景:
① 解决易用性问题 封装底层的复杂实现;
② 解决性能问题 将多个接口组装为一个大而全接口,减少网络开销;
③维护遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互, 提高复用性。
me商品中心使用广泛,但是有些 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); } }
9.6、组合模式(不常用)
概念:将对象组合成树状结构以表示 “整体-部分”的层次关系。
使用场景:用来处理树状结构数据。
- 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式;业务需求可以通过在树上的递归遍历算法来实现。
- 如果节点和叶子有很多差异性的话,比如很多方法和属性不一样, 不适合使用组合模式
组合模式的角色及职责
- Component:这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理Component 子部件, Component 可以是抽象类或者接口
- Leaf:在组合中表示叶子节点,叶子结点没有子节点;
3)Composite:非叶子节点,用于存储子部件,在 Component 接口实现子部件的相关操作,比如增加、删除。
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; }
9.7、享元模式(被共享的单元)
定义:运用共享技术有效地支持大量细粒度的对象。
- 当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
- 享元角色:存储在享元对象内部且不会随环境而改变
- 不可共享角色:随环境改变而改变、不可共享的状态。
使用场景:各类池技术: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 方法创建。
- JDK库中的Integer cache(-128~127)
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
10、行为型设计模式
创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)
10.1、观察者模式(也称发布订阅模式,za开发常用)
概念:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
应用场景:非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。
观察者模式的实现方式:
1、同步阻塞的实现方式:编码层次的解耦
2、观察者模式异步非阻塞实现方式:EventBus
- 在商品中心代码中大量被应用
3、观察者模式跨进程的实现方式(不同的两系统):
- 基于消息队列来实现
补充:EventBus原理(在商品中心大量使用)
EventBus 中两个核心函数 register() 和 post() 的实现原理。
最关键的一个数据结构是 Observer 注册表,记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者的时候,EventBus 通过解析@Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 Java 的反射语法来动态地创建对象、执行函数。对于同步阻塞模式,EventBus 在一个线程内依次执行相应的函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数。
demo1:
10.2、模板方法模式
模板方法模式的定义:在一个方法中定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
模板模式有两大作用:复用和扩展。
复用指的是,所有的子类可以复用父类中提供的模板方法的代码。
扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
回调:回调是一种双向调用关系
A 类事先注册某个函数 F 到 B 类,A类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。
结论:回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。
10.3、策略模式:就是将几个类中公共的方法提取到一个新的类中,从而使扩展更容易,保证代码的可移植性
策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。
因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
实现方式:
a)提供公共接口或抽象类,定义需要使用的策略方法。(策略抽象类)
b)多个实现的策略抽象类的实现类。(策略实现类)
c)环境类,对多个实现类的封装,提供接口类型的成员量,可以在客户端中切换。
d)客户端 调用环境类 进行不同策略的切换。
使用场景是什么?
Demo1 使用 Spring 原生注解,快速实现策略模式 + 工厂模式?
目前做的需求中,将业务逻辑梳理后抽离出来,借助Spring IOC依赖注入特性,使用到了策略模式 + 工厂模式,向外提供统一的调用方式,有效减少了 if/else 的业务代码,使得代码更易维护、拓展。
业务场景:微服务A中协议状态的变更,会影响微服务B中商品状态的变更。我们的期望目标是,根据不同协议状态,我们能够快速找到对应的策略实现类去执行对商品的操作。
快速实现
Step1 现在我们先定义个ProtocolChangeHandler 接口,并实现策略类
public interface ProtocolChangeHandler { /** * @param items 商品 * @param changeType 变更类型 */ void associateItemChange(List<Item> items, Byte changeType); }
两个策略类
@Component public class ItemShelfHandler implements ProtocolChangeHandler { @Override public void associateItemChange(List<Item> agItems, Byte changeType) { // todo 业务逻辑 } } @Component public class ItemUnShelfHandler implements ProtocolChangeHandler { @Override public void associateItemChange(List<Item> agItems, Byte changeType) { // todo 业务逻辑 } }
Step2 借助Spring 强大的依赖注入
下面的设计是消除if/else的关键代码,定义了一个StrategyHolder 来当做工厂类
@Component public class StrategyHolder { // 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中 @Autowired private Map<String, ProtocolChangeHandler> strategyMap; public ProtocolChangeHandler getBy(String entNum) { return strategyMap.get(entNum); } }
这个Map的key值就是你的 bean id,你可以用@Component(“value”)的方式设置,像我上面直接用默认的方式的话,就是首字母小写。value值则为对应的策略实现类。
先让用一个简单的启动类试一下。
public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class); context.getBean(StrategyHolder.class).getBy("itemShelfHandler").associateItemChange("",""); }
Step3 别名转换
启动类里面的“itemShelfHandler”填的实际上是bean id的值。那在实际业务中肯定是不会这样的,怎么可能把一个策略编号定义的这么奇怪呢?
我们可以利用 SpringBoot 的特性,通过配置文件的方式来修改建立 name 与 bean id 间的映射关系。
@Component @EnableConfigurationProperties @ConfigurationProperties(prefix = "ent") public class EntAlias { private HashMap<String, String> aliasMap; public static final String DEFAULT_STATEGY_NAME = "defaultStrategy"; public HashMap<String, String> getAliasMap() { return aliasMap; } public void setAliasMap(HashMap<String, String > aliasMap) { this.aliasMap = aliasMap; } String of(String entNum) { return aliasMap.get(entNum); } }
在对应配置文件application.yml中配置:
ent: aliasMap: entA: entAStrategy entB: entBStrategy
改写一下 ProtocolChangeHandler 类
@Component public class ProtocolChangeHandler { @Autowired private EntAlias entAlias; // 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中 @Autowired private Map<String, ProtocolChangeHandler> strategyMap; // 找不到对应的策略类,使用默认的 public ProtocolChangeHandler getBy(String entNum) { String name = entAlias.of(entNum); if (name == null) { return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME); } ProtocolChangeHandler strategy = strategyMap.get(name); if (strategy == null) { return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME); } return strategy; } }
10.4、责任链模式
定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系, 将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。
责任链模式的涉及初衷:是为了解耦代码,应对代码的复杂性,让代码满足开闭原则,提高代码的可扩展性
责任链模式的应用场景
最常用来开发框架的过滤器和拦截器。
案例1:对于支持 UGC(User Generated Content,用户生成内容)的应用(比如论坛)来说,用户生成的内容(比如,在论坛中发表的帖子)可能会包含一些敏感词(比如涉黄、广告、反动等词汇)。针对这个应用场景,我们就可以利用职责链模式来过滤这些敏感词。对于包含敏感词的场景,我们直接禁止发布。
public interface SensitiveWordFilter { boolean doFilter(Content content); } public class SexyWordFilter implements SensitiveWordFilter { @Override public boolean doFilter(Content content) { boolean legal = true; //... return legal; } } // PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似 public class SensitiveWordFilterChain { private List<SensitiveWordFilter> filters = new ArrayList<>(); public void addFilter(SensitiveWordFilter filter) { this.filters.add(filter); } //return true if content doesn't contain sensitive words. public boolean filter(Content content) { for(SensitiveWordFilter filter : filters) { if(!filter.doFilter(content)){ return false; } } return true; } } public class ApplicationDemo { public static void main(String[] args) { SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain(); filterChain.addFilter(new AdsWordFilter()); filterChain.addFilter(new SexyWordFilter()); filterChain.addFilter(new PoliticalWordFilter()); boolean legal = filterChain.filter(new Content()); if(!legal) { //不发表 } else { // 发表 } } }
案例2:责任链模式在商品中心的应用
- 在Spring初始化时,被注册了10来个规则,放在规则引擎集合里面
在发商品和更新商品/spu时调用
// 设计的不友好,破坏了规则引擎,既要保证性能又要满足审核时去掉校验规则,所以只能硬编码处理 // 只有申请变更才会走handleInboundAuditData逻辑,审核单独走自己的校验规则 // 校验数据,一旦数据不合法, 会抛出异常 //* 重要: 可以根据需求而定是否抛出异常, //* 例如可能的需求有: //* 1.忽略或者过滤用户提交的无效数据 //* 2. 可以处理用户提交的数据, 比如修正或者添加更多的信息 if(Objects.nonNull(auditState) && Objects.equals(GoodsAuditStateEnum.IN_UPDATE.getCode(),auditState)){ ruleEngine.handleInboundAuditData(newFullItem, null); }else { ruleEngine.handleInboundData(newFullItem, null); }
在用户查询商品/spu, 或者进入编辑商品/spu的界面
//* 规则引擎在处理数据输出时会调用这个方法 (例如用户查询商品/spu, 或者进入编辑商品/spu的界面) //* 处理数据, 如果数据不合法, 不会抛出异常, 而是根据规则做相应的修正 //* 目前的策略是, 根据规则本身来修正或者过滤, 或者添加信息, 也可以根据需要抛出异常 public <T extends BaseInput> void handleOutboundData(T input, BaseOutput output) { for (GeneralRuleExecutor ruleExecutor : ruleExecutorPipeline.getRuleExecutors()) { ruleExecutor.handleOutboundData(input, output, propertyBusinessTagCode); } }
责任链模式在Spring中的使用
1、servlet中的filter:定义一个Chain链,里面包含了Filter列表和servlet,达到在调用真正servlet之前进行各种filter逻辑
提供规范,其实现依赖于web容器,例如tomcat,ApplicationFilterChain 类就是 Tomcat 提供的 FilterChain 的实现类。
2、dubbo中的filter :把Filter封装成 Invoker的匿名类,通过链表这样的数据结构来完成责任链,调用的时候我们只知道第一个节点,每个节点包含了下一个调用的节点信息
3、亮点:mybatis中的plugin(插件):与过滤器类似,例如,我们在项目里面用到了mybatis的分页插件pagehelper,相当于执行Sql语句的时候做一些操作。
4、netty中的channelhandler和pipeline:共同构成了责任链模式
6、Spring中的Interceptor
可以讨论的问题? Spring AOP 是基于代理模式来实现的。在实际的项目开发中,我们可以利用 AOP 来实现访问控制功能,比如鉴权、限流、日志等。而Servlet Filter、Spring Interceptor 也可以用来实现访问控制。那在项目开发中,类似权限这样的访问控制功能,我们该选择三者(AOP、Servlet Filter、Spring Interceptor)中的哪个来实现呢?有什么参考标准吗?
Filter 可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息; Interceptor 可以拿到你请求的控制器和方法,却拿不到请求方法的参数; Aop 可以拿到方法的参数,但是却拿不到http请求和响应的对象。
要区分三者的特点,Spring AOP的使用粒度是类,是对类的一个包装;servlet filter 和 spring interceptor主要是对httpRequest、httpResponse做处理,servlet filterChain的实现依赖于具体的Web容器,而spring interceptor和spring AOP都依赖于spring框架,servlet filter在一个函数里拦截请求和响应,而spring interceptor将请求、响应的拦截分成了两个函数;其次,针对特定的应用场景,选择适合的。
demo?
10.5、状态模式
什么是状态模式?
- 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用if… ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状体(if else(或switch case)语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。这时就要考虑只修改自身状态的模式。
类图
1)环境角色(Context)
客户程序需要的接口,并且维护一个具体状态的实例,这个实例决定当前状态。
2)状态角色(State)
定义一个接口以封装与使用环境角色的一个特定状态的相关行为。
定义一个接口用以封装对象的一个特定状态所对应的行为。
3)具体状态角色(ConcreteState)
实现状态角色定义的接口,结构十分简单与策略模式相似。
一个具体的状态的实现类实现了Context对象的一个状态所对应的行为
使用场景:
《重构:改善既有代码的设计》中第一个例子
使用场景1:
- 在日志系统中可以用起来(对于各条业务线:都有上架、下架、冻结、开启、关闭、解冻、删除等状态)。
使用场景2:
- 我们在购物网站进行购物时,订单会产生几种状况:已下单、已付款、送货中、确定收货等状态。所以系统会判断该订单的状态,不管是哪种状态都应给出对应的操作,这就是状态
使用场景3:
- 商品状态(上架、下架、冻结、解冻)
状态模式优缺点
优点:
- 每个状态都是一个子类,只要增加状态就要增加子类,修改状态,只修改一个子类即可。
- 结构清晰,避免了过多的switch…case或者if…else语句的使用,避免了程序的复杂性,提高可维护性。
- State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。
缺点:
1) 状态模式的使用必然会增加系统类和对象的个数。
2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
状态模式面试题:
金融借贷平台项目:借贷平台的订单,有审核-发布-抢单 等等 步骤,随着操作的不同,会改变订单的状态, 项目中的这个模块实现就会使用到状态模式,请你使用状态模式进行设计,并完成实际代码
状态模式和策略模式比较
在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转换。
而策略模式里,采取何种策略由外部条件(C)决定。Strategy模式与State模式的结构形式完全一样。但它们的应用场景(目的)却不一样,State模式重在强调对象的内部状态的变化改变对象的行为,Strategy模式重在外部对策略的选择,策略的选择由外部条件决定,也就是说算法动态的切换。但由它们的结构是如此的相似。我们可以认为状态模式是完全封装且自修改的策略模式。
所以说策略和状态模式是孪生兄弟。
状态模式的适用性
- 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
- 代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
JDK中用到的状态模式
java.util.Iterator
迭代器模式的角色组成
- 迭代器角色(Iterator):迭代器角色负责定义访问和遍历元素的接口。
- 具体迭代器角色(Concrete Iterator):具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置。
- 容器角色(Container):容器角色负责提供创建具体迭代器角色的接口。
- 具体容器角色(Concrete Container):具体容器角色实现创建具体迭代器角色的接口—这个具体迭代器角色与该容器的结构相关。
代码示例(错误):
public class Order { public static void main(String [] args) { // 关于订单状态的事情,大家有没有这样判断过 String stateName = getStateName(1); System.out.println(stateName); } /** * @method getStateName * @description 获取订单状态的方法 * 订单状态1、已下单2、已付款3、已发货4、送货中5、确认收货 * @date: 2018/12/19 22:04 * @author: Ni Shichao * @param status 状态 * @return */ public static String getStateName (int status) { String stateName = "" ; if (status == 1) { stateName = "已下单"; } else if (status == 2) { stateName = "已付款"; } else if (status == 3) { stateName = "已发货"; } else if (status == 4) { stateName = "送货中"; } else if (status == 5) { stateName = "确认收货"; } return stateName; } }
代码示例(状态模式的使用):
// 1、定义state接口 public interface State { void handle(); } // 2、定义一个环境类来维护State接口 public class Context { private State state; // 无参构造 public Context() {} // 有参构造 public Context(State state) { this.state = state; } public void setState(State state) { System.out.println("订单信息已更新!"); this.state = state; this.state.handle(); } } // 3、具体状态角色 ConcreteState public class Booked implements State { @Override public void handle() { System.out.println("您已下单!"); } } //4、测试类 public class Client { public static void main(String [] args) { Context context = new Context(); context.setState(new Booked()); context.setState(new Payed()); context.setState(new Sended()); context.setState(new InWay()); context.setState(new Recieved()); } }
测试结果
订单信息已更新! 您已下单! 订单信息已更新! 您已付款! 订单信息已更新! 已发货! 订单信息已更新! 送货中。。。 订单信息已更新! 已确认收货!
状态模式在商品审核中的应用
// 状态机的定义 public class ItemFSM { // 事件 private enum Trigger { APPLY_ONSHELF, // 申请上架 APPLY_UNFREEZE, // 申请解冻 UNFREEZE, // 解冻 CANCEL_AUDIT, // 撤销审核 FREEZE // 撤销审核 } private final StateMachine<ItemStatus, Trigger> stateMachine; public ItemFSM(ItemStatus status) throws Exception { this.stateMachine = new StateMachine<>(status); /** * 【冻结】---(申请解冻)---> 【解冻审核中】*/ stateMachine.Configure(ItemStatus.FROZEN) .Permit(Trigger.APPLY_UNFREEZE, ItemStatus.WAIT_UNFREEZE); /*** 【解冻审核中】---(解冻)---> 【下架】*/ stateMachine.Configure(ItemStatus.WAIT_UNFREEZE) .Permit(Trigger.UNFREEZE, ItemStatus.UNDERSHELF); /*** 【待审核】---(取消审核)---> 【下架】*/ stateMachine.Configure(ItemStatus.AUDITWAIT) .Permit(Trigger.CANCEL_AUDIT, ItemStatus.UNDERSHELF); /*** 【上架】---(冻结)---> 【冻结中】*/ stateMachine.Configure(ItemStatus.ONSHELF) .Permit(Trigger.FREEZE, ItemStatus.FROZEN); /*** 【下架】---(申请上架)---> 【审核中】*/ stateMachine.Configure(ItemStatus.UNDERSHELF) .Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT); /** * 【审核失败】---(申请上架)---> 【审核中】*/ stateMachine.Configure(ItemStatus.AUDITREJECT) .Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT); } /*** 获取状态机当前状态*/ public ItemStatus getCurrentState() { return stateMachine.getState(); } /** * 申请上架*/ public void applyOnshelf() throws StateMachineException { try { stateMachine.Fire(Trigger.APPLY_ONSHELF); } catch (Exception e) { log.warn("item audit failed", e); throw new StateMachineException("item audit statemachine trigger apply unfreeze."); } } ... } // 状态机的使用 ItemStatus originalStatus = ItemStatus.from(blockItem.getStatus()); ItemFSM fsm = new ItemFSM(originalStatus); fsm.applyOnshelf(); ItemStatus newStatus = fsm.getCurrentState(); ...
总结
状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。
10.6、迭代器模式
问题:
1、在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什
么呢?如何来解决这个问题呢?
- 使用 foreach 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。
- 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为结果不可预期行为或者未决行为。
2、除了编程语言中基础类库提供的针对集合对象的迭代器之外,实际上,迭代器还有其他的应用场景,比如MySQL ResultSet 类提供的 first()、last()、previous() 等方法,也可以看作一种迭代器,你能分析一下它的代码实现吗?
- ResultSet 内部通过维护一个类型为 ResultSetRows 的 rowData 变量来实现迭代,而 rowData 的迭代方法本质就是实现了标准库的 Iterator 接口。
demo1:
10.7、发布-订阅(Publish/Subscribe)模式-访问者模式(不常用)
dubbo,服务中间件 zookeeper
实现方式:
a)角色抽象类(提供对观察者的添加,删除和通知功能)。
b)角色具体类,实现a,维护一个c的集合(对角色抽象类的实现)。
c)观察者抽象类(被角色通知后实现的方法)。
d)观察者实现类,实现c(多个)。
10.9、备忘录模式(不常用)
10.8、命令模式
10.9、解释器模式(不常用)
面试题:
1、介绍解释器设计模式是什么?
2、画出解释器设计模式的UML类图,分析设计模式中的各个角色是什么?
3、请说明Spring的框架中,哪里使用到了解释器设计模式,并做源码级别的分析。
Spring框架中 SpelExpressionParser就使用到解释器模式。
10.10 中介模式(不常用)
定义:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
EventBus 框架解析
行为型设计模式总结
11、在实际项目开发中,如何避免过度设计?
12、各框架源码中,使用了哪些设计模式
12.1 JDK源码里面都有些什么让你印象深刻的设计模式使用,举例看看?
1、抽象工厂模式 (通过创造性的方法来识别工厂本身,这又可以用于创建另一个抽象/接口类型)
- javax.xml.parsers.DocumentBuilderFactory#newInstance()
- javax.xml.transform.TransformerFactory#newInstance()
- javax.xml.xpath.XPathFactory#newInstance()
2、建造者模式 (通过创建方法识别返回实例本身)
- java.lang.StringBuilder#append() (非线程安全)
- java.lang.StringBuffer#append() (线程安全)
3、工厂模式 (可通过创建方法识别返回抽象/接口类型的实现)
- java.text.NumberFormat#getInstance()
- java.nio.charset.Charset#forName()
4、单例模式(通过创造性方法识别,每次返回相同的实例(通常是自己))
- java.lang.Runtime#getRuntime()
- java.awt.Desktop#getDesktop()
12.2 Spring源码里面都有些什么让你印象深刻的设计模式使用,举例看看?
使用了这些设计模式,具体请看这篇文章
Spring从入门到精通
12.3 Guava源码中使用了哪些设计模式
请看这篇文章
二十多岁刚进入职场时,总要工作到头昏眼花,才有做了工作的感觉,着实白费了相当多的时间。虽说年轻时体力充沛,像那样的工作方式,也算有助于心情愉快。然而说穿了,其实那也不过是自我陶醉而已,所谓的收获可能也只是了解到自己体力的极限,只有在确实产生出有意义的成果(输出)之后才能获得成长。若能持续有价值的工作,并保持其质量,就算“偷工减料”也完全不成问题。如果是问人就可以解决的事,那么问人就好;如果有更简单的完成工作的方法,就该换个方式处理。 --《麦肯锡教我的思考武器:从逻辑思考到真正解决问题》