设计模式中篇之 23 种设计模式
# 设计模式
# 前言
学习设计模式的方法
第一部分是 应用场景,即这个模式可以解决哪类问题
第二部分是 解决方案,即这个模式的设计思路和具体的代码实现
不过,代码实现并不是模式必须包含的。 如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉,比如
1. 中介模式与观察者模式的区别
2. 装饰器模式与代理模式的区别
3. 工厂模式与建造者模式的区别
常见的设计模式有 23 种,大体分为三种类型:创建型、结构型、行为型
创建型:主要用于解决对象的创建过程
结构型:把类或对象通过某种形式结合在一起,构成某种复杂或合理的结构(类或对象的组合)
行为型:主要用来解决类或对象之间的交互,以更合理地优化类或对象之间的关系(类或对象的交互)
# 创建型
# 单例模式 🌟
定义
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式
实现单例模式需要注意的四点
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例
- 考虑对象创建时的线程安全问题
- 考虑是否支持延迟加载
- 考虑 getInstance() 性能是否高(是否加锁)
单例模式分类
- 饿汉式
- 在类加载的时候,instance 静态实例就已经创建并初始化好了
- 不支持延迟加载
- 懒汉式
- 支持延迟加载
- 但由于加锁,这会导致产生性能问题
- 双重检测
- 是饿汉式和懒汉式的结合
- 支持延迟加载
- 初次调用才会加锁,后续调用不会加锁,所以性能也不会很差
- 静态内部类
- 支持延迟加载
- 线程安全
- 枚举
- 利用枚举类型本身的特性
- 饿汉式
代码实现
// 1. 饿汉式
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
// 静态常量,在类加载时就会创建类实例
private static final IdGenerator instance = new IdGenerator();
// 构造函数必须是 private 访问权限
private IdGenerator() {}
// 外部通过此方法来获取单例实例
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// 2. 懒汉式
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
// 构造函数必须是 private 访问权限
private IdGenerator() {}
// 外部通过该方法调用获取单例实例
public static synchronized IdGenerator getInstance() {
if (instance == null) { // 第一次调用不存在,所以需要创建实例,后续再调用时复用该实例
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// 3. 双重检测
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
// 构造函数必须是 private 访问权限
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) { // 第一次调用才会加锁,后续调用不会加锁
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// 4. 静态内部类
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
// 构造函数必须是 private 访问权限
private IdGenerator() {}
// 静态内部类
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
// 当调用该函数时,静态内部类才会被加载并实例化 IdGenerator 对象
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
// 5. 枚举
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
# 工厂模式 🌟
工厂的理解:批量生产东西的地方
批量:多个
生产:创建
东西:对象
对应到代码层面就是:当需要创建多个不同的对象时,我们可以将创建多个不同的对象的逻辑封装到一个单独的类中,这个类我们称之为工厂类
实际上当单个对象的创建逻辑比较复杂的时候,我们也可以将单个对象的创建逻辑封装到一个单独的类中 这种方法就是工厂模式,将对象的创建与使用进行分离
分类
- 简单工厂模式
- 可看成是工厂方法的一种特例
- 工厂方法模式
- 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类
- 抽象工厂模式
- 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类
- 简单工厂模式
DI 容器
定义
DI 容器底层最基本的设计思路就是基于工厂模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。
核心功能
- 配置解析
- 对象创建
- 对象生命周期管理
# 建造者模式 🌟
应用场景
我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
与工厂模式的区别
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
# 原型模式
定义
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。
浅拷贝与深拷贝
# 结构型
# 代理模式 🌟
定义
它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类添加附加功能
静态代理
- 通过实现接口:原始类和代理类实现相同的接口,然后代理类添加附加功能
- 通过继承:代理类继承原始类,然后代理类添加附加功能
动态代理
- 不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类
- 动态代理是为了解决没有接口的原始类,它们多数是那些别人已经写好封装起来的。
# 桥接模式
定义
将 抽象 和 实现 解耦,让它们可以独立变化
抽象与实现的理解
抽象:定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。
实现:而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起
# 装饰器模式 🌟
装饰器模式与代理模式的区别?
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
# 适配器模式 🌟
定义
它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作
主要用于将老的系统升级为新的系统,同时保留原有接口的使用习惯
分类
- 类适配器
- 基于继承
- 对象适配器
- 基于组合(依赖注入)
- 类适配器
应用场景
- 兼容老的版本接口
# 门面模式
定义
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。 主要用于处理接口粒度的设计,接口粒度不能太大,也不能太小,需要对其做一个权衡
应用场景
- 解决易用性问题
- 解决性能问题
- 解决分布式事务问题
# 组合模式
定义
将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。 组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者)可以统一单个对象和组合对象的处理逻辑 主要用于处理树形结构数据
应用场景
- 文件系统
- 文件
- 目录:目录之下有子目录和文件
- OA 系统
- 文件系统
# 享元模式
定义
所谓“享元”,顾名思义就是被 共享的单元。
享元模式的意图是复用对象以节省内存,但其前提是享元对象是不可变对象。
应用场景
- 棋盘
- 文本编辑器
- Java Integer 类
Integer i1 = 78; Integer i2 = 78; Integer i3 = 129; Integer i4 = 129; System.out.println(i1 == i2); // true System.out.println(i3 == i4); // false
- Java String 类(字符串常量池)
String s1 = "lkjisme"; String s2 = "lkjisme"; String s3 = new String("lkjisme"); System.out.println(s1==s2); // true System.out.println(s1==s3); // false
# 行为型
# 观察者模式 🌟
定义
观察者模式又称 发布订阅模式 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知,在这里“一”表示的是被观察者只有一个,“多”表示的是观察者有多个 基于此衍生出来各种实现方式,其中发布订阅模式最为著名,它通过引入中间系统(如订阅中心、消息队列等)将观察者与被观察者进行解耦,提高了代码的可扩展性、可维护性
实现方式分类
- 同步阻塞
- 异步非阻塞
- 进程间
- 跨进程
应用场景
- EventBus
# 模板模式
定义
模板模式,全称是模板方法设计模式 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤 用于解决 复用 与 扩展 两个问题
- 复用指的是,所有的子类可以复用父类中提供的模板方法的代码
- 扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能
应用场景
- Java InputStream
- Java AbstractList
- Java Servlet
- JUnit TestCase
与 callback 的区别与联系
# 策略模式
策略模式与工厂模式的结合与联系
定义
定义一族算法类,将每个算法分别封装起来,让它们可以互相替换 策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码) 策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多(这里体现了单一职责原则,高内聚、低耦合的设计思想)
策略组成
- 定义:包含一个策略接口和一组实现这个接口的策略类
- 创建:借用工厂模式来封装创建各种策略类的逻辑
- 使用
- 运行时动态确定使用哪个策略
- 非运行时确定使用哪个策略
应用场景
# 职责链模式 🌟
定义
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 更加通俗地讲就是在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
应用场景
框架中的过滤器、拦截器
- Axios 中的请求拦截和响应拦截
- Express 中的中间件
框架中的插件扩展
- Vue 中的插件(使用 Vue.use 方法注册插件)
- Pinia 中的插件(使用 pinia.use 方法注册插件)
- MyBatis Plugin
# 状态模式 🌟
状态模式只是状态机的一种实现方式,因此有必要先学习一些状态机的相关概念
什么是状态机 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机 状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作
状态机的两种表示
- 状态转移图
- 二维表
状态机的三种实现方式
- 分支逻辑法 参照状态转移图,将每一个状态转移,原模原样地直译成代码,也是最简单直接的实现方式
- 查表法
- 状态模式
# 迭代器模式 🌟
迭代器是用来遍历容器的,所以一个完整的迭代器模式应该包含两个部分:容器和容器迭代器
举例
代码实现(体现了 基于接口而非实现编程 的设计思想)
// 容器接口
public interface List<E> {
Iterator iterator();
//...省略其他接口函数...
}
// 迭代器接口
public interface Iterator<E> {
boolean hasNext();
void next();
E currentItem();
}
// 容器实现类
public class ArrayList<E> implements List<E> {
public Iterator iterator() {
return new ArrayIterator(this);
}
//...省略其他代码
}
// 迭代器实现类
public class ArrayIterator<E> implements Iterator<E> {
private int cursor;
private ArrayList<E> arrayList;
public ArrayIterator(ArrayList<E> arrayList) {
this.cursor = 0;
this.arrayList = arrayList;
}
@Override
public boolean hasNext() {
return cursor != arrayList.size(); // 注意这里,cursor在指向最后一个元素的时候,hasNext()仍旧返回true。
}
@Override
public void next() {
cursor++;
}
@Override
public E currentItem() {
if (cursor >= arrayList.size()) {
throw new NoSuchElementException();
}
return arrayList.get(cursor);
}
}
// Demo 演示
public class Demo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("xzg");
names.add("wang");
names.add("zheng");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.currentItem());
iterator.next();
}
}
}
遍历集合一般有三种方式
for 循环
foreach 循环
迭代器遍历
后两种本质上属于一种,都可以看作迭代器遍历
# 访问者模式
定义
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身
应用场景
一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
双分派与访问者模式的关系
- 单分派
- 双分派
- 编译时与运行时
- 函数重载
# 备忘录模式
定义
备忘录模式,也叫快照(Snapshot)模式 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态
应用场景
主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计
# 命令模式
定义
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能
应用场景
- Netty
- Redis
# 解释器模式
定义
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
应用场景
自定义监控告警规则
- 自定义告警规则相当于是一种语法规则
- 解释器负责处理这种语法规则
规则引擎
# 中介模式
定义
中介模式定义了一个单独的中介对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
与观察者模式的区别
在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是,大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。也就是说,在观察者模式的应用场景中,参与者之间的交互关系比较有条理。
而中介模式正好相反。只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式