设计模式中篇之 23 种设计模式

5/21/2023 partern

# 设计模式

  1. 数据结构与算法:教你如何写出高效的代码

  2. 设计模式:教你如何写出高质量的代码

SoSo 若将 数据结构与算法设计模式 两者修炼至第九层巅峰境界会是怎样??

# 前言

学习设计模式的方法

  1. 第一部分是 应用场景,即这个模式可以解决哪类问题

  2. 第二部分是 解决方案,即这个模式的设计思路和具体的代码实现

不过,代码实现并不是模式必须包含的。 如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉,比如

1. 中介模式与观察者模式的区别

2. 装饰器模式与代理模式的区别

3. 工厂模式与建造者模式的区别

常见的设计模式有 23 种,大体分为三种类型:创建型、结构型、行为型

  1. 创建型:主要用于解决对象的创建过程

  2. 结构型:把类或对象通过某种形式结合在一起,构成某种复杂或合理的结构(类或对象的组合)

  3. 行为型:主要用来解决类或对象之间的交互,以更合理地优化类或对象之间的关系(类或对象的交互)

# 创建型

# 单例模式 🌟

  1. 定义

    一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式

  2. 实现单例模式需要注意的四点

    1. 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例
    2. 考虑对象创建时的线程安全问题
    3. 考虑是否支持延迟加载
    4. 考虑 getInstance() 性能是否高(是否加锁)
  3. 单例模式分类

    1. 饿汉式
      • 在类加载的时候,instance 静态实例就已经创建并初始化好了
      • 不支持延迟加载
    2. 懒汉式
      • 支持延迟加载
      • 但由于加锁,这会导致产生性能问题
    3. 双重检测
      • 是饿汉式和懒汉式的结合
      • 支持延迟加载
      • 初次调用才会加锁,后续调用不会加锁,所以性能也不会很差
    4. 静态内部类
      • 支持延迟加载
      • 线程安全
    5. 枚举
      • 利用枚举类型本身的特性
  4. 代码实现

// 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();
  }
}

# 工厂模式 🌟

工厂的理解:批量生产东西的地方

  1. 批量:多个

  2. 生产:创建

  3. 东西:对象

对应到代码层面就是:当需要创建多个不同的对象时,我们可以将创建多个不同的对象的逻辑封装到一个单独的类中,这个类我们称之为工厂类

实际上当单个对象的创建逻辑比较复杂的时候,我们也可以将单个对象的创建逻辑封装到一个单独的类中 这种方法就是工厂模式,将对象的创建与使用进行分离

  1. 分类

    1. 简单工厂模式
      • 可看成是工厂方法的一种特例
    2. 工厂方法模式
      • 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类
    3. 抽象工厂模式
      • 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类
  2. DI 容器

    1. 定义

      DI 容器底层最基本的设计思路就是基于工厂模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。

    2. 核心功能

      • 配置解析
      • 对象创建
      • 对象生命周期管理

# 建造者模式 🌟

  1. 应用场景

    1. 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。

    2. 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。

    3. 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。

  2. 与工厂模式的区别

    1. 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。

    2. 建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。

    1

# 原型模式

  1. 定义

    如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

  2. 浅拷贝与深拷贝

# 结构型

# 代理模式 🌟

  1. 定义

    它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类添加附加功能

  2. 静态代理

    1. 通过实现接口:原始类和代理类实现相同的接口,然后代理类添加附加功能
    2. 通过继承:代理类继承原始类,然后代理类添加附加功能
  3. 动态代理

    1. 不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类
    2. 动态代理是为了解决没有接口的原始类,它们多数是那些别人已经写好封装起来的。

# 桥接模式

  1. 定义

    将 抽象 和 实现 解耦,让它们可以独立变化

  2. 抽象与实现的理解

    1. 抽象:定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。

    2. 实现:而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起

# 装饰器模式 🌟

装饰器模式与代理模式的区别?

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

# 适配器模式 🌟

  1. 定义

    它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作

    主要用于将老的系统升级为新的系统,同时保留原有接口的使用习惯

  2. 分类

    1. 类适配器
      1. 基于继承
    2. 对象适配器
      1. 基于组合(依赖注入)
  3. 应用场景

    1. 兼容老的版本接口

# 门面模式

  1. 定义

    门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。 主要用于处理接口粒度的设计,接口粒度不能太大,也不能太小,需要对其做一个权衡

  2. 应用场景

    1. 解决易用性问题
    2. 解决性能问题
    3. 解决分布式事务问题

# 组合模式

  1. 定义

    将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。 组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者)可以统一单个对象和组合对象的处理逻辑 主要用于处理树形结构数据

  2. 应用场景

    1. 文件系统
      1. 文件
      2. 目录:目录之下有子目录和文件
    2. OA 系统

# 享元模式

  1. 定义

    所谓“享元”,顾名思义就是被 共享的单元

    享元模式的意图是复用对象以节省内存,但其前提是享元对象是不可变对象。

  2. 应用场景

    1. 棋盘
    2. 文本编辑器
    3. 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
      
    
    1. 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
    

# 行为型

# 观察者模式 🌟

  1. 定义

    观察者模式又称 发布订阅模式 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知,在这里“一”表示的是被观察者只有一个,“多”表示的是观察者有多个 基于此衍生出来各种实现方式,其中发布订阅模式最为著名,它通过引入中间系统(如订阅中心、消息队列等)将观察者与被观察者进行解耦,提高了代码的可扩展性、可维护性

  2. 实现方式分类

    1. 同步阻塞
    2. 异步非阻塞
    3. 进程间
    4. 跨进程
  3. 应用场景

    1. EventBus

# 模板模式

  1. 定义

    模板模式,全称是模板方法设计模式 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤 用于解决 复用 与 扩展 两个问题

    • 复用指的是,所有的子类可以复用父类中提供的模板方法的代码
    • 扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能
  2. 应用场景

    1. Java InputStream
    2. Java AbstractList
    3. Java Servlet
    4. JUnit TestCase
  3. 与 callback 的区别与联系

# 策略模式

策略模式与工厂模式的结合与联系

  1. 定义

    定义一族算法类,将每个算法分别封装起来,让它们可以互相替换 策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码) 策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多(这里体现了单一职责原则,高内聚、低耦合的设计思想)

  2. 策略组成

    1. 定义:包含一个策略接口和一组实现这个接口的策略类
    2. 创建:借用工厂模式来封装创建各种策略类的逻辑
    3. 使用
      1. 运行时动态确定使用哪个策略
      2. 非运行时确定使用哪个策略
  3. 应用场景

# 职责链模式 🌟

  1. 定义

    将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 更加通俗地讲就是在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

  2. 应用场景

    1. 框架中的过滤器、拦截器

      1. Axios 中的请求拦截和响应拦截
      2. Express 中的中间件
    2. 框架中的插件扩展

      1. Vue 中的插件(使用 Vue.use 方法注册插件)
      2. Pinia 中的插件(使用 pinia.use 方法注册插件)
      3. MyBatis Plugin

# 状态模式 🌟

状态模式只是状态机的一种实现方式,因此有必要先学习一些状态机的相关概念

  1. 什么是状态机 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机 状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作

  2. 状态机的两种表示

    1. 状态转移图
    2. 二维表
  3. 状态机的三种实现方式

    1. 分支逻辑法 参照状态转移图,将每一个状态转移,原模原样地直译成代码,也是最简单直接的实现方式
    2. 查表法
    3. 状态模式

# 迭代器模式 🌟

迭代器是用来遍历容器的,所以一个完整的迭代器模式应该包含两个部分:容器和容器迭代器

2

举例

1

代码实现(体现了 基于接口而非实现编程 的设计思想)


// 容器接口
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();
    }
  }
}

遍历集合一般有三种方式

  1. for 循环

  2. foreach 循环

  3. 迭代器遍历

后两种本质上属于一种,都可以看作迭代器遍历

# 访问者模式

  1. 定义

    允许一个或者多个操作应用到一组对象上,解耦操作和对象本身

  2. 应用场景

    一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。

  3. 双分派与访问者模式的关系

    1. 单分派
    2. 双分派
    3. 编译时与运行时
    4. 函数重载

# 备忘录模式

  1. 定义

    备忘录模式,也叫快照(Snapshot)模式 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态

  2. 应用场景

    主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计

# 命令模式

  1. 定义

    命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能

  2. 应用场景

    1. Netty
    2. Redis

# 解释器模式

  1. 定义

    解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

  2. 应用场景

    1. 自定义监控告警规则

      1. 自定义告警规则相当于是一种语法规则
      2. 解释器负责处理这种语法规则
    2. 规则引擎

# 中介模式

  1. 定义

    中介模式定义了一个单独的中介对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

    3

  2. 与观察者模式的区别

    1. 在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是,大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。也就是说,在观察者模式的应用场景中,参与者之间的交互关系比较有条理。

    2. 而中介模式正好相反。只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式

Last Updated: 5/24/2023, 7:15:29 PM