23 种经典设计模式共分为 3 种类型,分别是创建型、结构型和行为型。
结构型设计模式
结构型模式就是一些类或对象组合在一起的经典结构,用来解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。
1,代理模式
在不改变原始类(或叫被代理类)的情况下,通过引入代理类来给原始类来附加非业务功能。一般情况下,我们让代理类和原始类实现同样的接口。但是,如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的。在这种情况下,我们可以通过让代理类继承原始类的方法来实现代理模式。
静态代理需要针对每个类都创建一个代理类,增加了维护成本和开发成本。
于是有了动态代理。不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
Spring AOP 底层的实现原理就是基于动态代理。配置好需要给哪些类创建代理,并定义好在执行原始类的业务代码前后执行哪些附加功能。Spring 为这些类创建动态代理对象,并在 JVM 中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法,也就实现了给原始类添加附加功能的目的。
代理模式的应用场景:
1. 业务系统的非功能性需求开发,比如:监控、统计、鉴权、限流、事务、幂等、日志。将这些附加功能与业务功能解耦,放到代理类中统一处理,程序员只需要关注业务方面的开发。
2.代理模式在 RPC、缓存中的应用,略。
2,桥接模式(基于组合)
--“将抽象和实现解耦,让它们可以独立变化。”
另一种理解方式:“一个类存在两个以上独立变化的维度,我们通过组合的方式,让这多个维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。类似于“组合优于继承”设计原则。
JDBC 驱动是桥接模式的经典应用。
抽象和实现:JDBC 本身就相当于“抽象”(这里的“抽象”,指的并非“抽象类”或“接口”,而是跟具体的数据库无关的、被抽象出来的一套“类库”)。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。(这里的“实现”,也并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”)。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。
3,装饰器模式(基于组合)
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。
Java IO 类的设计是装饰器模式的经典应用。(例如还有mybatis中的cache)
从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。
第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类。
第二个比较特殊的地方是:装饰器类是对功能的增强(代理模式是对业务和非业务功能的解耦),这也是装饰器模式应用场景的一个重要特点。
4,适配器模式
适配器模式是用来做适配,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”,如果在设计初期,就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。
会出现接口不兼容的 5 种场景:
封装有缺陷的接口设计
统一多个类的接口
设计替换依赖的外部系统
兼容老版本接口
适配不同格式的数据
例如:日志框架Slf4j
比如,项目中用到的某个组件使用 log4j 来打印日志,项目本身使用的是 logback。将组件引入到项目之后,项目就相当于有了两套日志打印框架。每种日志框架都有自己特有的配置方式,要针对每种日志框架编写不同的配置文件。所以,为了解决这个问题,需要统一日志打印框架。
Slf4j 这个日志框架相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback)来使用。Slf4j 不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器,对不同日志框架的接口进行二次封装,适配成统一的 Slf4j 接口定义。
所以,可以统一使用 Slf4j 提供的接口来编写打印日志的代码,具体使用哪种日志框架实现(log4j、logback),是可以动态地指定的。
代理、桥接、装饰器、适配器 4 种设计模式的区别
这4种模式代码结构非常相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
5,门面模式
门面模式是为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
接口粒度设计得太大,太小都不好。太大会导致接口不可复用,太小会导致接口不易用。在实际的开发中,接口的可复用性和易用性需要“微妙”的权衡。一个基本的处理原则是,尽量保持接口的可复用性,但针对特殊情况,允许提供冗余的门面接口,来提供更易用的接口。
1. 解决易用性问题
Linux 的 Shell 命令,实际上可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。
2. 解决性能问题
过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。
3. 解决分布式事务问题
除了可以通过引入分布式事务框架或者事后补偿的机制来解决之外,还可以利用数据库事务或者 Spring 框架提供的事务,在一个事务中,执行多个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。
6,组合模式
组合模式是指将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合模式让使用者可以统一单个对象和组合对象的处理逻辑。
组合模式的设计思路,可认为是对业务场景的一种数据结构和算法的抽象。数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
组织成树形结构后,就可以将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。
7,享元模式
享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
不仅仅相同对象可以设计成享元,对于相似对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。
享元模式 VS 单例、缓存、对象池
单例模式是为了保证对象全局唯一。享元模式是为了实现对象复用,节省内存。缓存是为了提高访问效率,而非复用。池化技术中的“复用”理解为“重复使用”,主要是为了节省时间。
享元模式在 Java Integer 中的应用
Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);
输出结果:true(享元,地址相同),false(地址不同)。
在 Java Integer 的实现中,-128 到 127 之间的整型对象会被事先创建好,缓存在 IntegerCache 类中。当使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。这里的 IntegerCache 类就是享元工厂类,事先创建好的整型对象就是享元对象。
享元模式在 Java String 中的应用
String s1 = "str";
String s2 = "str";
String s3 = new String("str");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
输出结果:true(享元,地址相同),false(地址不同)。
在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量。
网友评论