java设计模式详解——23种设计模式

启示录

   上一篇对Spring6进行了详细的解读,最后发现各种设计模式混在一起难免有些混淆,所以决定单独开一篇设计模式,基于网上资料和B站课程进行同步整理和学习,这里选用的课程是黑马的java设计模式。

一、设计模式的分类

1. 创建型模式

📌 定义:关注对象的创建机制,用于灵活的创建对象方式,并且隐藏具体的创建细节。

📌 常见模式:(Spring的简单工厂原理类似,但不是23种设计模式之一

  • 单例(Singleton)
  • 原型(Property)
  • 工厂方法(Factory Method)
  • 抽象工厂(Abstract Factory)
  • 建造者(Builder)

2. 结构型模式

📌 关注类和对象的组合方式,形成一种更大的结构。

📌 常见模式:

  • 适配器(Adapter)
  • 代理(Proxy)
  • 桥接(Bridge)
  • 装饰器(Decorator)
  • 外观(Facade)
  • 享元(Flyweight)
  • 组合(Composite)

3. 行为型模式

📌 关注对象间的通信和职责分配,定义对象如何交互和分配任务。

📌 常见模式:

  • 模板方法(Template Method)
  • 策略(Strategy)
  • 命令(Command)
  • 职责链(Chain of Responsibility)
  • 状态(State)
  • 观察者(Observer)
  • 中介者(Mediator)
  • 迭代器(Iterator)
  • 访问者(Visitor)
  • 备忘录(Memento)
  • 解释器(Interpreter)

4. 结构型模式和行为型模式的区别

结构型模式(Structural) 行为型模式(Behavioral)
核心目标 解决类和对象的组合方式(静态结构) 解决对象间的交互与职责分配(动态行为)
关注点 如何通过继承、组合、接口等搭建代码结构 如何定义对象间的通信、状态变化、算法流程
代码侧重点 类/对象的关系设计(如适配、代理、装饰) 对象间的协作逻辑(如观察、策略、命令)
生活类比 电脑硬件的组装(接口适配、部件组合) 电脑运行时软硬件的协作(温度监控、任务调度)
典型模式 适配器、桥接、组合、装饰器、代理 观察者、策略、命令、状态、责任链
关键问题 如何组织代码结构? 对象如何交互和分工?

二、如何读懂类图

1. 基础属性

💡 类图作为软件设计中的核心部分,是需要我们掌握的,虽然平时可能不用,但是必须要能看懂,就算不能完全的记住,也要学会怎么快速的通过资料掌握。

在这里插入图片描述

在这里插入图片描述

  • 类名:用首字母大写表示,在矩形的最高层
  • 接口名:用<<interface>>表示,依旧首字母大写,在矩形最高层
  • 属性:矩形第二层,一般为可见性 名称 :类型 [ = 缺省值]
  • 方法:矩形最后一层,一般为可见性 名称(参数列表) [ :返回类型]

💡可见性一般分为三种

  • +:表示public
  • -:表示private
  • #:表示protected

2. 类、接口之间的关系

关系类型 表示法 箭头方向 代码表现 强度 生活例子 典型场景
继承 ◁— 实线 子类 → 父类 class A extends B 猫是一种动物 通用特性抽象
实现 ◁.. 虚线 实现类 → 接口 class A implements B 鸟实现飞行能力 多态设计
关联 —→ 实线(可带角色名) A → B class A { b: B } 老师持有学生列表 长期持有的对象引用
聚合 ◇— 空心菱形 + 实线 整体 → 部分 class A { bs: List<B> } 汽车包含轮胎(可更换) 部分可独立存在
组合 ◆— 实心菱形 + 实线 整体 → 部分 class A { b: B } 最强 房子包含房间(同生死) 部分生命周期依赖整体
依赖 ..→ 虚线 A → B 方法参数/局部变量 人临时使用书本 临时性协作

💡 符号说明

  • ◁—:空心三角箭头 + 实线
  • ◇—:空心菱形 + 实线
  • ◆—:实心菱形 + 实线
  • 箭头方向:始终指向被依赖/被关联方

2.1 继承

在这里插入图片描述

classDiagram

    Vehicle <|-- Car : 继承
    
    class Vehicle {
        +move(): void
    }
    class Car {
        +accelerate(): void
    }

2.2 实现

在这里插入图片描述

classDiagram

    Engine <|.. ElectricEngine : 实现
    
    class Engine {
        <<interface>>
        +start(): void
    }
    class ElectricEngine {
        +start(): void
    }

2.3 关联

在这里插入图片描述

classDiagram

    Car "1" --> "1" Driver : 关联
    
    class Car {
        -driver: Driver
    }
    class Driver {
        -name: String
    }

2.4 聚合

在这里插入图片描述

classDiagram

    University "1" o-- "*" Student : 聚合
    
    class University {
        -students: List~Student~
    }
    class Student {
        -name: String
    }

2.5 组合

在这里插入图片描述

classDiagram

    House "1" *-- "*" Room : 组合
    
    class House {
        -rooms: List~Room~
    }
    class Room {
        -area: int
    }

2.6 依赖

在这里插入图片描述

classDiagram

    Car ..> Fuel : 依赖
    
    class Car {
        +refuel(f: Fuel): void
    }
    class Fuel {
        -type: String
    }

2.7 完整示例

在这里插入图片描述

三、软件设计原则

直接点击软件设计原则即可跳转,上一篇文章已经详细介绍过。

四、创建者模式

1. 单例模式

📌 定义:单例模式确保一个类只有一个实例,并提供全局访问点,常用于管理共享资源(如数据库连接、线程池等)等。

核心特性

  • 唯一性:一个类只能有一个实例。
  • 全局访问:通过静态方法或属性获取该实例。
  • 延迟访问(可选):可以选择在第一次请求时初始化实例。

实现方式

1. 懒汉式(线程不安全)
public class Singleton {
    private static Singleton instance;
    
    private Singleton() {} // 私有构造方法
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

❗️ 这种创建方式在多线程下是不安全的,也就是说在同一个线程中这个对象是单例的,只有一个实例,但是在多个线程中就会存在多个实例,拿Tomcat为例,如果一次请求为一个线程,那并发量高的话,你这个对象就会存在多份,服务器压力会很大。

2. 饿汉式(线程安全)
public class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

优点: 实现简单,通过final修饰,确保只存在一份实例,且线程安全

缺点: 在类初始化时就会实例化,可能浪费资源。

💡 其实在正常开发中,我们大多数使用的是这种饿汉式,因为简单粗暴,而且在大多数情况下,类初始化时实例化这点开销可以忽略不计。

3. 双重检查锁(DCL线程安全)
public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查
            synchronized (Singleton.class) {     // 加锁
                if (instance == null) {          // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优点:延迟初始化 + 线程安全 + 高性能。
关键点volatile 防止指令重排序。

注意 ❗️ ❗️ ❗️

这里是一个难点,底层原理涉及比较深,但是挺有意思的。

volatile关键字详解(扩展)

💡 作用:保证变量的可见性禁止指令重排序,但不保证其原子性,它在多线程编程中用于解决内存可见性问题,但不适用于需要原子操作的场景(如 i++)

变量可见性(Visibility)

问题背景

在 Java 内存模型(JMM)中,每个线程有自己的工作内存(缓存),线程操作变量时可能直接从缓存读取,而不是主内存。这可能导致一个线程修改了变量,但其他线程看不到最新值

class Example {
    boolean flag = false; // 不使用 volatile
    void writer() { flag = true; }
    void reader() { while (!flag); } // 可能死循环,读不到 flag 的变化
}

解决方案

volatile 修饰的变量,所有读写直接操作主内存,保证修改后立即对其他线程可见。

volatile boolean flag = false; // 使用 volatile
禁止指令重排序(Ordering)

问题背景

JVM 和 CPU 为了提高性能,可能会对指令进行重排序(如单例模式的 Double-Checked Locking 问题)。

class Singleton {
    private static Singleton instance;
    static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 可能发生指令重排序
                }
            }
        }
        return instance;
    }
}

new Singleton() 的步骤:

  • 分配内存空间

  • 初始化对象

  • 将引用指向内存地址

如果发生重排序(1→3→2),其他线程可能拿到未初始化的对象。

解决方案

使用 volatile 修饰变量,禁止 JVM 进行指令重排序。

private static volatile Singleton instance; // 解决 DCL 问题

💡 到底什么是重排序?重排序的执行者是谁?重排序的执行时机是什么?

💡 重排序就是一种优化机制,核心就是调换两行代码的执行顺序(前提是两行代码之间没有必要的关联,比如int a = 1;int b = 2;这种),可以适当提高效率,但是这种情况下在多线程就会存在问题,类似上面说到的问题。

💡 重排序的三种类型

类型 执行者 触发场景 示例
编译器重排序 Java/JIT 编译器 编译源代码时,对没有数据依赖的语句进行优化 交换int a=1;int b=2;的执行顺序
指令级重排序 CPU处理器 运行时指令流水线采用乱序执行(Out-of-Order Execution)优化 先执行b=1再执行a=data.getValue()以隐藏内存访问延迟
内存系统重排序 缓存一致性协议 多核CPU的缓存同步延迟导致写操作可见顺序与程序顺序不一致 线程A写变量x后,线程B可能先看到y的更新后看到x的更新

关键特征对比

  1. 编译器重排序

    • 发生在代码编译阶段
    • 通过调整语句顺序优化寄存器使用
    • 示例:int x=1; int y=x;不会被重排序(存在数据依赖)
  2. CPU指令重排序

    • 发生在运行时指令执行阶段
    • 现代CPU普遍支持乱序执行
    • 示例:Store-Load可能被重排序(写后读操作)
  3. 内存系统重排序

    • 由缓存一致性协议(MESI等)引起
    • 本质是内存可见性问题
    • 示例:单例DCL问题中,可能先看到对象引用后看到初始化内容

如何避免重排序导致的问题

方法 作用机制 适用场景 代码示例
volatile 变量 禁止指令重排序 + 保证可见性 状态标志位、DCL单例模式 private volatile boolean ready;
synchronized 内存屏障 + 原子性保障 复合操作/临界区保护 synchronized(lock){ counter++; }
final 字段 构造函数初始化安全 不可变对象创建 final Map config = initConfig();
原子类 CAS操作 + volatile语义 计数器等简单原子操作 AtomicInteger count = new AtomicInteger(0);

各方案对比说明

  1. volatile方案

    • ✅ 轻量级,仅针对变量操作
    • ❌ 不保证复合操作的原子性
    • 典型应用:
      // 双重检查锁定(DCL)
      private volatile static Singleton instance;
      
  2. synchronized方案

    • ✅ 全能解决方案(原子性+可见性)
    • ❌ 有性能开销
    • 典型应用:
      public synchronized void safeIncrement(){
          counter++;
      }
      
  3. final方案

    • ✅ 初始化安全性(JLS 17.5保证)
    • ❌ 仅适用于不可变对象
    • 典型应用:
      class SafeConfig {
          final Map<String, String> config;
          SafeConfig() {
              config = loadConfig(); // 构造函数内安全发布
          }
      }
      
  4. 原子类方案

    • ✅ 无锁高性能
    • ❌ 仅适用于简单操作
    • 典型应用:
      AtomicReference<State> state = new AtomicReference<>(INIT);
      
4. 静态内部类(线程安全)
public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

优点:懒加载 + 无锁线程安全(利用类加载机制)。

💡 首先我们要明白,我们使用的是Holder.INSTANCE变量,而这是通过内部类实现的,只有你调用getInstance方法才会执行,重点是jvm的类加载机制是天然线程安全的,它只会保证你这个内部类加载一次,所以非常的方便好用(不懂的去了解一下static变量和类加载机制,这里不做赘述)

5. 枚举(线程安全)
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("Singleton action");
    }
}

使用方式

Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();

优点:

  • 绝对防止反射和序列化破坏单例,反序列化也是直接返回当前实例(java规定)。

  • 代码简洁,JVM 保证唯一性,因为底层就是用final + static修饰的。

为什么枚举单例是最佳实践?
  1. 绝对线程安全
    由 JVM 类加载机制 保证,无需任何同步代码(如 synchronized 或 volatile)。

  2. 天然防反射攻击
    枚举的构造方法 无法通过反射调用(Constructor.newInstance() 直接报错)。

  3. 天然防反序列化攻击
    枚举的反序列化 不会创建新对象,而是直接返回已存在的实例。

  4. 代码最简洁
    只需一行 INSTANCE 定义,无需额外处理同步、反射、序列化等问题。

扩展:反序列化攻击

这一块大多数初级程序员可能都懵懵懂懂,什么是反序列化?什么又是反序列化攻击

简单说一下,反序列化就是我们发送请求到服务器之后,这其实是一串序列化之后的编码,也就是将一些字符串和数据编码后的格式,服务器通过规则解析成java或者其它语言可以识别的格式,而反序列化攻击就是在发送的数据上做手脚,然后通过你反序列化的漏洞将危险代码植入到你的服务器,比如。

// 攻击者构造的恶意代码
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", 
        new Class[]{String.class, Class[].class}, 
        new Object[]{"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke", 
        new Class[]{Object.class, Object[].class}, 
        new Object[]{null, new Object[].class}),
    new InvokerTransformer("exec", 
        new Class[]{String.class}, 
        new Object[]{"calc.exe"}) // 弹出计算器
};

Transformer chain = new ChainedTransformer(transformers);
Map payload = new HashMap();
payload.put(chain, "dummy");

// 序列化成字节流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(payload);
byte[] maliciousData = bos.toByteArray();

// 通过HTTPS发送给你的服务
sendHttpsRequest(maliciousData);

当你的服务反序列化这个数据时

  1. 重建HashMap对象

  2. 自动调用HashMap的readObject()

  3. 触发Transformer链式调用

  4. 最终执行Runtime.getRuntime().exec(“calc.exe”)

这是一个简单的例子,是commons-collections的一个漏洞,现已经修复。我想表达的是,或许我们用不到这个知识,但你得知道,在将来的某一天,你成为了开发组长或者经理,你不能不知道这些攻击的原理,不能不知道该怎么办。

应用场景

  1. 配置管理(全局配置,例如AppConfig)
  2. 日志记录(单例日志记录器)
  3. 数据库连接池(避免重复链接)
  4. 缓存系统(Redis)

破坏单例的方式

  1. 反射(在构造方法中增加判断 if (instance != null)
  2. 序列化(使用readResolve可避免)

2. 原型模式

📌 定义:它允许通过复制现有对象(原型)来创建新对象,而不是通过 new 实例化,适用场景:

  • 对象创建成本高(数据库查询、计算等)
  • 需要动态运行时创建对象
  • 避免构造函数的约束(创建对象和构造函数没有关系了)

实现方式

2.1 浅拷贝
class Person implements Cloneable {
    private String name;
    private int age;
    private List<String> hobbies; // 引用类型(浅拷贝会共享)

    public Person(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        this.hobbies = hobbies;
    }

    @Override
    public Person clone() {
        try {
            return (Person) super.clone(); // 调用Object的clone()
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    // Getters & Setters...
}
  • 适用于没有嵌套对象,结构简单的对象。
  • 仅仅复制对象的内存地址和简单的属性
  • 如果原始对象修改,复制的所有对象都会被修改
2.2 深拷贝

完全复制对象及其引用的所有对象,适用于嵌套对象的场景

方式1:手动复制

@Override
public Person clone() {
    try {
        Person cloned = (Person) super.clone();
        // 将嵌套的对象都在这手动赋值
        cloned.hobbies = new ArrayList<>(this.hobbies); // 创建新List
        return cloned;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

方式2:序列化拷贝(推荐,但是要注意实现方式)

💡 优点:通用性强,适合复杂对象。

import java.io.*;

class Person implements Serializable {
    // ...(同上)

    public Person deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("Deep clone failed", e);
        }
    }
}
  • 上面的例子是自己写的序列化拷贝,但是在正式开发中不推荐,使用第三方库更安全,比如Jackson。
  • 上面的方式一般是不会存在反序列化攻击的,如果你的拷贝对象完全来自于jvm那就不存在这种危险,如果你的对象是来自于网络则需要注意,所以使用第三方库更加安全。

原型模式的应用场景

场景 说明
对象创建开销大 当直接通过 new 创建对象成本较高时(如复杂计算、数据库查询),通过复制已有对象提升性能。
动态配置对象 运行时根据需求修改原型对象的属性,快速生成不同配置的新对象。
撤销/恢复功能 保存对象的历史状态(如文档编辑器中的“撤销”操作),通过克隆实现状态回滚。
游戏开发 快速生成大量相似实体(如敌人、子弹、道具),避免重复初始化资源。
多线程环境 需要线程安全的对象实例时,可预先克隆备用的独立对象。
保护性拷贝 防止外部代码修改原始对象(如返回防御性副本)。

原型模式的优缺点

优点 缺点
1. 性能高:比直接 new 对象更高效(尤其适用于复杂对象)。 1. 深拷贝实现复杂:需递归处理所有嵌套对象,代码量增加。
2. 动态性:运行时灵活克隆对象,无需依赖子类。 2. 需实现 Cloneable:违反迪米特法则(侵入性设计)。
3. 绕过构造函数限制:可复制包含私有构造方法的对象。 3. 浅拷贝风险:默认 clone() 是浅拷贝,共享引用对象可能导致错误。
4. 减少重复初始化:复用原型对象的初始化状态。 4. 循环引用问题:深拷贝时需处理对象间的循环依赖。

迪米特法则的核心思想

迪米特法则强调“最少知识原则”:一个对象应该只与其直接朋友(成员变量、方法参数、返回值等)交互,而不应了解其他对象的内部结构或依赖关系。目的是降低耦合。

💡 上面说的违反迪米特法则其实正常来说允许适当出现,因为这个法则是尽量避免我们操作对象中的子对象,因为你不知道子对象是否实现了序列化接口,是否实现了克隆接口,或者说子对象中还有没有内部对象,比较容易造成循环依赖的问题。

💡 在Spring的BeanUtils中有一个方法是copyProperties,这个方法就是用来复制对象的,可能会出现循环依赖现象,如果出现,可以通过@JsonIgnore注解忽略互相引用的字段,或者在复制时指定忽略的字段BeanUtils.copyProperties(source, target, "b"); // 忽略字段 "b"

💡 当然,因为上面的方法底层原理还是反射,如果你的业务要求高(比如在游戏中,一个人物有一个技能是分身),这时候使用反射效率就很低了,可以采用封装DTO的形式或者说使用MapStruct 和 Orika等第三方库避免(了解即可,如果用到自行百度),这两个会在编译时形成一个字节码库,相当于手写的DTO,很快。

3. 工厂模式

直接点击 工厂模式 即可跳转,上一篇文章已经详细介绍过。

关于 简单工厂 也可以了解一下,Spring中常用的一种模式。

4. 抽象工厂模式

点击 抽象工厂 即可跳转,介绍比较详细且易懂。

5. 建造者模式

📌 定义: 将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。

📌 和工厂模式的区别

  • 工厂模式倾向于结果,客户端提供需求,工厂直接提供结果。
  • 建造者模式倾向于过程,将一个复杂对象分步骤创建,返回给客户端。

包含的角色

  • Product(产品):表示被创建的复杂对象。
  • Builder(抽象构造者):定义创建产品各个部件的抽象构造方法。
  • ConcreteBuilder(具体构造者):Builder的实现类,用于创建具体的部件。
  • Director(指挥官):调度员,构建一个使用Builder接口的对象。

优缺点

💡 优点

  • 封装性好:建造过程与产品分离。
  • 扩展性好:新增具体建造者不用修改原有的代码。
  • 便于控制细节:可以更好的控制不同部件的创建细节。
  • 解耦:将客户端与包含多个部件的复杂对象创建过程分离。

❌ 缺点

  • 产品必须有共同点
  • 增加了系统的复杂度:创建多个Builder类
  • 适用范围有限:适合复杂对象

代码示例

// 产品类 - 电脑
class Computer {
    private String cpu;
    private String ram;
    private String storage;
    private String gpu;
    
    // 省略getter和setter方法
    
    @Override
    public String toString() {
        return "Computer{" +
                "cpu='" + cpu + '\'' +
                ", ram='" + ram + '\'' +
                ", storage='" + storage + '\'' +
                ", gpu='" + gpu + '\'' +
                '}';
    }
}

// 抽象建造者
interface ComputerBuilder {
    void buildCPU();
    void buildRAM();
    void buildStorage();
    void buildGPU();
    Computer getResult();
}

// 具体建造者 - 游戏电脑
class GamingComputerBuilder implements ComputerBuilder {
    private Computer computer = new Computer();
    
    @Override
    public void buildCPU() {
        computer.setCpu("Intel i9");
    }
    
    @Override
    public void buildRAM() {
        computer.setRam("32GB DDR5");
    }
    
    @Override
    public void buildStorage() {
        computer.setStorage("1TB NVMe SSD");
    }
    
    @Override
    public void buildGPU() {
        computer.setGpu("NVIDIA RTX 4090");
    }
    
    @Override
    public Computer getResult() {
        return computer;
    }
}

// 具体建造者 - 办公电脑
class OfficeComputerBuilder implements ComputerBuilder {
    private Computer computer = new Computer();
    
    @Override
    public void buildCPU() {
        computer.setCpu("Intel i5");
    }
    
    @Override
    public void buildRAM() {
        computer.setRam("16GB DDR4");
    }
    
    @Override
    public void buildStorage() {
        computer.setStorage("512GB SSD");
    }
    
    @Override
    public void buildGPU() {
        computer.setGpu("Integrated Graphics");
    }
    
    @Override
    public Computer getResult() {
        return computer;
    }
}

// 指挥者
class ComputerDirector {
    public Computer construct(ComputerBuilder builder) {
        builder.buildCPU();
        builder.buildRAM();
        builder.buildStorage();
        builder.buildGPU();
        return builder.getResult();
    }
}

// 客户端代码
public class BuilderPatternDemo {
    public static void main(String[] args) {
        ComputerDirector director = new ComputerDirector();
        
        // 构建游戏电脑
        ComputerBuilder gamingBuilder = new GamingComputerBuilder();
        Computer gamingComputer = director.construct(gamingBuilder);
        System.out.println("Gaming Computer: " + gamingComputer);
        
        // 构建办公电脑
        ComputerBuilder officeBuilder = new OfficeComputerBuilder();
        Computer officeComputer = director.construct(officeBuilder);
        System.out.println("Office Computer: " + officeComputer);
    }
}

适用场景

  1. 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时

  2. 当构造过程必须允许被构造的对象有不同的表示时(同一个产品,实现细节不同,表示不同)

  3. 当需要生成的产品对象有复杂的内部结构

  4. 当需要生成的产品对象的属性相互依赖,需要指定其生成顺序时

扩展

  1. 当过程固定或者流程较少,可以省略Director。
  2. 链式调用:Builder的方法可以返回this,实现链式调用
public class ComputerBuilder {
    private Computer computer = new Computer();
    
    public ComputerBuilder withCPU(String cpu) {
        computer.setCpu(cpu);
        return this;
    }
    
    public ComputerBuilder withRAM(String ram) {
        computer.setRam(ram);
        return this;
    }
    
    public Computer build() {
        return computer;
    }
}

实际应用案例

  • Java中的StringBuilder:append()链式调用的经典案例

  • Android中的AlertDialog.Builder

  • Lombok的@Builder注解

  • MyBatis中的SqlSessionFactoryBuilder

计算公式应用案例

📌 现在要实现业务上的计算功能,你是最终计算的出口,需要汇总多个子业务的结果并给出计算结果和分析结果,我们就可以给出一个计算抽象建造者例如CalcBuilder。

interface CalcBuilder {
	List<Object> getParams();
    
    List<String> getFormula();

	BigDecimal calc();
}

📌 这是一个简单的例子,后续子业务根据抽象建造者构建自己的计算部分,完成后由调用者汇总再计算,也是一个非常典型的应用案例。

五、结构型模式

关注类和对象的组合方式,形成一种更大的结构。

1. 适配器模式(Adapter)

📌 定义:它允许不兼容的接口之间能够协同工作

📌 举例:手机充电器,因为你的手机接受不了220V电压

┌─────────────┐     ┌──────────────────┐     ┌──────────────┐
│   Client    │───▶ │    Target        │     │   Adaptee    │
└─────────────┘     ├──────────────────┤     ├──────────────┤
                    │ + request():void │     │ + specific() │
                    └────────┬─────────┘     └──────┬───────┘
                             △                      │
                             │                      │
                      ┌──────┴─────────┐            │
                      │   Adapter      │────────────┘
                      ├────────────────┤
                      │ + request():void│
                      │   (调用specific)│
                      └────────────────┘

本质剖析

📌 核心价值

  • 接口转换:解决现有接口与需求接口不匹配问题

  • 兼容升级:保护现有投资,复用已有功能

  • 透明接入:客户端无需知道适配过程

📌 实现方式

  • 类适配器,通过继承实现。
  • 对象适配器,通过接口实现。

支付系统兼容案例

将老版银联支付(UPIPayment)接入新版支付系统(PaymentGateway)

  1. 目标接口
// 现代支付网关标准接口
public interface PaymentGateway {
    void processPayment(String merchantId, BigDecimal amount);
    String checkBalance(String accountId);
}
  1. 被适配类
// 旧版银联支付类
public class UPIPayment {
    public void makePayment(String accountFrom, String accountTo, double amount) {
        System.out.printf("转账 %s → %s : %.2f 元\n", 
            accountFrom, accountTo, amount);
    }
    
    public double queryBalance(String accountNo) {
        return new Random().nextDouble() * 10000;
    }
}
  1. 对象适配器
public class UPIPaymentAdapter implements PaymentGateway {
    private final UPIPayment upiPayment;
    
    public UPIPaymentAdapter(UPIPayment upiPayment) {
        this.upiPayment = upiPayment;
    }
    
    @Override
    public void processPayment(String merchantId, BigDecimal amount) {
        // 将商户ID视为收款方账号,固定系统账号为付款方
        upiPayment.makePayment("SYSTEM_ACCOUNT", merchantId, 
                              amount.doubleValue());
    }
    
    @Override
    public String checkBalance(String accountId) {
        double balance = upiPayment.queryBalance(accountId);
        return String.format("账户 %s 余额: %.2f", accountId, balance);
    }
}
  1. 类适配器(对比,不推荐)
// 通过继承实现(Java单继承限制需注意)
public class UPIClassAdapter extends UPIPayment implements PaymentGateway {
    @Override
    public void processPayment(String merchantId, BigDecimal amount) {
        super.makePayment("SYSTEM_ACCOUNT", merchantId, 
                        amount.doubleValue());
    }
    
    @Override
    public String checkBalance(String accountId) {
        return String.format("账户 %s 余额: %.2f", 
                          accountId, super.queryBalance(accountId));
    }
}
  1. 客户端调用
public class PaymentClient {
    public static void main(String[] args) {
        // 使用对象适配器
        PaymentGateway gateway1 = new UPIPaymentAdapter(new UPIPayment());
        gateway1.processPayment("MERCHANT_001", new BigDecimal("99.50"));
        System.out.println(gateway1.checkBalance("USER_123"));
        
        // 使用类适配器
        PaymentGateway gateway2 = new UPIClassAdapter();
        gateway2.processPayment("MERCHANT_002", new BigDecimal("199.00"));
        System.out.println(gateway2.checkBalance("USER_456"));
    }
}

模式关键点对比

对象适配器 类适配器
实现方式 组合(持有Adaptee对象引用) 继承(extends Adaptee类)
灵活性 更高(可动态更换Adaptee) 较低(编译时绑定)
方法覆盖 需显式委托调用Adaptee方法 可直接重写父类方法
多Adaptee支持 支持(可组合多个Adaptee) 不支持(单继承限制)
语言限制 无(普遍适用) 受单继承限制(如Java)
代码复杂度 较高(需手动转发调用) 较低(直接继承方法)
运行时开销 略高(多一层调用) 较低(直接方法调用)
推荐程度 ★★★★★(更灵活) ★★★☆☆(特定场景使用)

java中使用案例

最经典的当属SpringMvc中的HandlerAdapter了,可以阅读下面这篇文章,相当的详细。

SpringMVC中HandlerMapping和HandlerAdapter详解(适配器模式)

典型应用场景

  • 遗留系统集成
  • 第三方库适配
  • 接口版本过渡

2. 代理模式(Proxy)

点击 代理模式 即可跳转,之前给Spring6中讲过,包含静态代理和动态代理。

3. 桥接模式(Bridge)

📌 定义:将抽象部分实现部分分离,使得他们可以独立变化。它提供了一种结构让两种在各自的继承条件下建立连接,但不是通过继承的关系(继承限制性太大)。

包含的角色

  • 抽象化(Abstraction):定义抽象接口,维护一个对实现化对象的引用装配一个对象,可以调用实现对象的方法,这个对象是桥接器的核心)。

  • 扩展抽象化(Refined Abstraction):抽象化的子类,扩展或修正抽象化的定义。

  • 实现化(Implementor):定义实现类的接口,通常比抽象化接口更基本。

  • 具体实现化(Concrete Implementor):实现实现化接口的具体类。

在这里插入图片描述

模式优点

  • 分离抽象接口及其实现部分,提高了系统的可扩展性。

  • 可以独立地扩展抽象部分和实现部分。

  • 实现过程对客户透明隐藏了具体实现细节

  • 避免了多层继承带来的子类爆炸问题

模式缺点

  • 增加了系统的理解与设计难度。

  • 需要正确地识别系统中两个独立变化的维度

适用场景

  • 当一个类存在两个或多个独立变化的维度,且这两个维度都需要进行扩展时

  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。

  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

❗️ ❗️ ❗️ 大家看到这里多半会比较懵逼,事实上我也懵了好久,但是不要气馁,我来表演一下,请大家跟着我的思路走。首先多读几遍上面的概念,不懂的先mark一下,多看一下例子就很好懂了。

❗️ ❗️ ❗️ 大白话讲桥接器,一定要理解桥接是什么意思,它的作用就是将两种不同的东西可以组合起来,这叫做桥接,重点是什么,这两种东西都是独立的,可以随意扩展,桥接器只是提供了一种载体。比如花篮,它可以装花,你给女朋友表白的999朵玫瑰,但是你还可以进行组合,和卡片、氛围灯组合,这些都是独立的。(注意:不要把花篮理解成容器,这里的核心是组合,不是容纳,花篮是让你组合多个东西的桥接器
❗️ ❗️ ❗️ 重点重点重点:一定要理解组合两个字,这是桥接器产生的根本原因。

颜色和形状举例

✏️ 三种颜色:红、黄、蓝(实现化对象)

✏️ 三种形状:圆形、方形、三角形(抽象化角色)

📝 需求:客户会在提供的颜色和形状中随机组合进行绘画,但我们也需要提供所有已经组合过的产品让客户直接使用。

📌 首先分析上面的角色分类,颜色和形状都可以作为抽象化角色(具体根据需求,一般比较基本的是实现化对象),这里我们将形状作为抽象化角色,颜色作为实现化对象。

📌 其次我们来分析维度,这非常清晰,颜色和形状就是两个单独的维度,而且会根据需求随机进行扩展和修改(比如渐变色就是修改)。而且在业务上非常的灵活,两种维度可以随机进行组合甚至多次组合。

📌 接下来看实现方式,因为需求中需要我们提供现成的组合产品,按照传统继承设计来说我们排除这6个基本的类还需要提供3 * 3 = 9 种组合类,相当于15种。

在这里插入图片描述

    OK,这样算下来,如果后续扩展那增加的东西就太多了,所以桥接模式应运而生,解决方案如下:

  • 水平拆分:将N个维度拆分为N个独立的继承体系

  • 组合代替继承:通过对象引用建立维度间联系

  • 运行时绑定:在运行时动态组合维度

在这里插入图片描述

// 实现化接口 - 颜色
interface Color {
    void applyColor();
}

// 具体实现化 - 红色
class RedColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color");
    }
}

// 具体实现化 - 绿色
class GreenColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying green color");
    }
}

// 抽象化 - 形状
abstract class Shape {
    protected Color color;
    
    public Shape(Color color) {
        this.color = color;
    }
    
    abstract public void draw();
}

// 扩展抽象化 - 圆形
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }
    
    @Override
    public void draw() {
        System.out.print("Drawing Circle. ");
        color.applyColor();
    }
}

// 扩展抽象化 - 矩形
class Rectangle extends Shape {
    public Rectangle(Color color) {
        super(color);
    }
    
    @Override
    public void draw() {
        System.out.print("Drawing Rectangle. ");
        color.applyColor();
    }
}

// 客户端代码
public class BridgePatternDemo {
    public static void main(String[] args) {
        Shape redCircle = new Circle(new RedColor());
        Shape greenRectangle = new Rectangle(new GreenColor());
        
        redCircle.draw();
        greenRectangle.draw();
    }
}

何时使用桥接模式?

  1. 存在多个独立变化维度时
  • 如:图形系统(形状×颜色×纹理)

  • 如:支付系统(支付方式×支付渠道×货币类型)

  1. 需要运行时切换实现时
  • 如:动态更换数据库驱动

  • 如:游戏中的实时画质切换

  1. 避免永久绑定抽象与实现时
  • 如:开发跨平台框架

  • 如:支持多供应商的API

💡 这是一种非常典型解耦思想,将抽象和实现单独开,各自扩展

4. 装饰器模式

📌 定义:动态的给一个对象添加额外的功能,同时又不改变原有对象的功能。通过创建一个装饰类,用来包装原有的类,添加额外的功能又不改变原有的对象。

核心思想

装饰器模式的核心是组合优于继承,通过将对象放入包含行为的特殊封装对象中来为原对象动态添加新行为,而不是通过继承来扩展功能(传统的实现是通过继承来增加功能)。

模式结构

在这里插入图片描述

角色说明

  • Component(抽象构件):定义对象的接口,可以给这些对象动态添加职责

  • ConcreteComponent(具体构件):定义具体的对象,可以动态添加职责

  • Decorator(抽象装饰类):继承/实现Component,并持有一个Component对象的引用

  • ConcreteDecorator(具体装饰类):向构件添加具体的职责

模式特点

优点💡

比继承更灵活:可以在运行时动态添加或撤销功能

避免类爆炸:通过组合多个简单装饰器可以实现复杂功能

符合开闭原则:无需修改原有代码即可扩展功能

可以嵌套装饰:多个装饰器可以层层嵌套

缺点 ❌

会产生许多小对象:增加了系统的复杂性

排查困难:多层装饰时,调试和排查问题较复杂

初始化配置复杂:创建对象时需要逐层包装

Java的IO流装饰器

// 模拟Java IO的装饰器模式实现
public interface InputStream {
    byte[] read(int size);
    void close();
}

// 基础流 - 文件输入流
public class FileInputStream implements InputStream {
    private String filePath;
    
    public FileInputStream(String filePath) {
        this.filePath = filePath;
        System.out.println("Opening file: " + filePath);
    }
    
    @Override
    public byte[] read(int size) {
        System.out.println("Reading " + size + " bytes from file");
        return new byte[size]; // 模拟读取
    }
    
    @Override
    public void close() {
        System.out.println("Closing file: " + filePath);
    }
}

// 缓冲装饰器
public class BufferedInputStream implements InputStream {
    private InputStream wrappedStream;
    
    public BufferedInputStream(InputStream stream) {
        this.wrappedStream = stream;
        System.out.println("Adding buffering capability");
    }
    
    @Override
    public byte[] read(int size) {
        System.out.println("Buffered read of " + size + " bytes");
        return wrappedStream.read(size);
    }
    
    @Override
    public void close() {
        wrappedStream.close();
    }
}

// 数据解压装饰器
public class GZIPInputStream implements InputStream {
    private InputStream wrappedStream;
    
    public GZIPInputStream(InputStream stream) {
        this.wrappedStream = stream;
        System.out.println("Adding GZIP decompression");
    }
    
    @Override
    public byte[] read(int size) {
        System.out.println("Decompressing data before reading");
        byte[] compressed = wrappedStream.read(size);
        return decompress(compressed); // 模拟解压
    }
    
    private byte[] decompress(byte[] data) {
        System.out.println("Decompressing data...");
        return data; // 实际应实现解压逻辑
    }
    
    @Override
    public void close() {
        wrappedStream.close();
    }
}

// 客户端使用
public class IOExample {
    public static void main(String[] args) {
        // 基础文件流
        InputStream fileStream = new FileInputStream("data.gz");
        
        // 添加缓冲功能
        InputStream bufferedStream = new BufferedInputStream(fileStream);
        
        // 添加解压功能
        InputStream gzipStream = new GZIPInputStream(bufferedStream);
        
        // 读取数据
        byte[] data = gzipStream.read(1024);
        System.out.println("Read " + data.length + " bytes");
        
        // 关闭流
        gzipStream.close();
    }
}

进阶使用

// 典型用法
InputStream in = new BufferedInputStream(
                  new GZIPInputStream(
                    new FileInputStream("test.gz")));

📌 注意看装饰器的实现,虽然也产生了一些类,但是它通过在构造方法中传入顶级的抽象接口从而达到包装的目的,可以更好的扩展功能,和上面的桥接器的类爆炸问题相似,传统的继承类增加的数量都是指数增加的,而装饰器则是线性增加。

📌 其实我们更多的可以发现,装饰器都是在开发中进行使用的,提供者一般是第三方系统,开发者可以根据情况自由的扩展。

装饰器模式与相似模式对比

装饰器 vs 适配器
  • 装饰器:不改变接口,增强功能
  • 适配器:改变接口,使不兼容接口能一起工作
装饰器 vs 代理
  • 装饰器:关注动态添加功能
  • 代理:关注控制访问,可能不增强功能
装饰器 vs 桥接
  • 装饰器:运行时动态组合
  • 桥接:设计时静态绑定

5. 外观模式

核心思想

📌 为一组复杂的子系统接口提供一个更高级的统一接口,使客户端更容易使用。

结构组成

  1. Facade(外观角色
  • 知道哪些子系统类负责处理请求
  • 将客户端的请求代理给适当的子系统对象
  1. Subsystem Classes(子系统角色
  • 实现子系统的功能
  • 处理由Facade对象指派的任务
  • 没有Facade的任何信息,即没有指向Facade的指针
// 子系统类1
class SubSystemOne {
    public void methodOne() {
        System.out.println("子系统方法一");
    }
}

// 子系统类2
class SubSystemTwo {
    public void methodTwo() {
        System.out.println("子系统方法二");
    }
}

// 子系统类3
class SubSystemThree {
    public void methodThree() {
        System.out.println("子系统方法三");
    }
}

// 外观类
class Facade {
    private SubSystemOne one;
    private SubSystemTwo two;
    private SubSystemThree three;
    
    public Facade() {
        one = new SubSystemOne();
        two = new SubSystemTwo();
        three = new SubSystemThree();
    }
    
    public void methodA() {
        System.out.println("方法组A() ---");
        one.methodOne();
        two.methodTwo();
    }
    
    public void methodB() {
        System.out.println("方法组B() ---");
        two.methodTwo();
        three.methodThree();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Facade facade = new Facade();
        
        facade.methodA();
        facade.methodB();
    }
}

优点

  1. 简化接口:减少客户端需要处理的对象的数量,使子系统使用起来更加简单
  2. 松耦合:将客户端与复杂的子系统解耦,提高子系统的独立性和可移植性
  3. 灵活性:子系统内部变化不会影响外观对象,只需调整外观类即可
  4. 层次化结构:为系统定义了一个清晰的入口,便于分层管理

缺点

  1. 不符合OCP开闭原则:一旦增加子系统的实现,那外观类或者客户端都要修改;
  2. 过度使用:一旦外观类可能会承担更多的职责,变成上帝类。

适用场景

  1. 当要为复杂子系统提供一个简单接口时

  2. 当客户端与抽象类的实现部分之间存在很大的依赖性时

  3. 当需要构建一个层次结构的子系统时,使用外观模式定义子系统的入口点

实际应用案例

  1. 计算机启动过程:用户只需按下电源键,外观模式隐藏了BIOS自检、操作系统加载等复杂过程

  2. 数据库访问:JDBC中的Connection接口可以看作是一个外观,隐藏了底层复杂的数据库连接过程

  3. 日志框架:SLF4J作为外观,隐藏了Log4j、Logback等具体实现

与其他模式的关系

与适配器模式:两者都包装了一些对象,但适配器试图改变接口使其可用,而外观提供简化接口

与中介者模式:都抽象了已有功能,但中介者目的是协调同事对象,外观是简化接口

与单例模式:通常一个外观类只需要一个实例,因此常与单例模式结合使用

与生活中的联系

👀 外观模式最适配的例子就是电脑的电源键了,它将下面各个复杂的子系统接口进行包装,用户只需要点一下,什么键盘、鼠标、屏幕、CPU、GPU等等子系统依次启动,可以发现它确实是上帝类,统筹旗下各个子系统的工作顺序等。

👀 其实再想想,像这种的例子很多,比如银行的柜台服务,那个柜员就是一个外观类,我们只需要提供需求(取钱)和证件,柜员会按照预定的流程帮我们操作。

👀 外观模式的实质就是为了简化操作,而且大部分都在内部和外部直接对接的时候最常见,这是生活中非常常见的,很容易理解。

6. 享元模式

温馨提示:

首先我会列举出网上的核心概念,这是必须的步骤,如果光看概念会懵逼的,但是懵逼不懵逼,你先别着急,看完它,然后咱看完最后的例子再回来,你就知道了,享元模式也就那么一回事儿

核心概念

📌 分离对象中可共享的状态(内在状态)和不可共享的状态(外在状态),通过共享内在状态来减少对象的数量

❗️ 注意:这里的核心是什么,一个是内在状态,无非就是相同的属性,而目的是为了共享这些相同的属性来减少对象的数量,核心在我看来就两个字,“减少”

关键术语

  • 内在状态(Intrinsic State):
  1. 存储在享元对象内部

  2. 不会随环境变化而改变

  3. 可以共享的部分

  • 外在状态(Extrinsic State):
  1. 取决于享元对象所处的环境

  2. 会随环境变化而改变

  3. 不可共享的部分(由客户端保存)

结构组成

  • Flyweight(抽象享元类):

📌 声明一个接口,通过它可以接受并作用于外部状态

  • ConcreteFlyweight(具体享元类):

📌 实现抽象享元接口

📌 为内部状态增加存储空间

📌 必须是可共享的

  • UnsharedConcreteFlyweight(非共享具体享元类):

📌 并非所有的享元子类都需要被共享

📌 可以有不共享的享元对象

  • FlyweightFactory(享元工厂类):

📌 创建并管理享元对象

📌 确保合理地共享享元对象

示例

// 1. 享元接口
interface Character {
    void display(int x, int y); // x, y 是外部状态(位置)
}

// 2. 具体享元类 (存储内部状态:字符和字体)
class ConcreteCharacter implements Character {
    private char character; // 内部状态
    private String font;    // 内部状态

    public ConcreteCharacter(char character, String font) {
        this.character = character;
        this.font = font;
    }

    @Override
    public void display(int x, int y) {
        // 结合内部状态 (character, font) 和外部状态 (x, y) 进行显示
        System.out.println("显示字符 '" + character + "' 使用字体 '" + font + "' 在位置 (" + x + ", " + y + ")");
    }
}

// 3. 享元工厂
class CharacterFactory {
    private Map<String, Character> pool = new HashMap<>();

    public Character getCharacter(char character, String font) {
        String key = character + "-" + font; // 基于内部状态生成唯一键
        if (!pool.containsKey(key)) {
            pool.put(key, new ConcreteCharacter(character, font));
        }
        return pool.get(key);
    }
}

// 4. 客户端使用
public class Client {
    public static void main(String[] args) {
        CharacterFactory factory = new CharacterFactory();

        // 创建大量 'A' 字符,使用相同字体
        Character char1 = factory.getCharacter('A', "Arial");
        Character char2 = factory.getCharacter('A', "Arial"); // 会共享同一个对象
        Character char3 = factory.getCharacter('B', "Arial"); // 不同字符,新对象

        char1.display(10, 20); // 传入外部状态:位置
        char2.display(15, 25);
        char3.display(30, 40);

        // 验证共享
        System.out.println("char1 == char2: " + (char1 == char2)); // true (同一个对象引用)
    }
}

优点

  1. 减少内存消耗:通过共享大量相似对象,显著减少内存中对象的数量

  2. 提高性能:减少了对象的创建和销毁开销

  3. 外部状态独立:可以将外部状态集中管理,与内部状态分离

  4. 系统扩展性好:享元模式通常使用工厂方法,便于扩展

缺点

  1. 增加系统复杂性:需要分离内部状态和外部状态

  2. 线程安全问题:共享对象需要考虑多线程环境下的同步问题

  3. 逻辑复杂化:需要维护一个享元池,增加了系统逻辑复杂度

适用场景

  1. 一个系统有大量相似对象,造成内存开销过大

  2. 对象的大多数状态可以外部化,可以剥离出外部状态

  3. 需要缓冲池的场景

实际应用案例

  1. 文字处理软件:每个字符可以作为一个享元对象,共享字体、大小等内在状态,位置等作为外在状态

  2. 游戏开发:大量相同或相似的游戏角色、子弹、粒子效果等

  3. Java字符串池:String类的intern()方法使用了享元模式

  4. 数据库连接池:共享连接对象

重点例子!!!

❓ 思考一个问题,如果现有一个对象MyObject,我把它放入一个List中一万次,请问这和我直接new一万次MyObject对象有什么区别?

List<MyObject> list = new ArrayList<>();
MyObject obj = factory.getFlyweight("key"); 

for (int i = 0; i < 10000; i++) {
    list.add(obj); 
}
  1. 首先这个List长度肯定是一万,但是前者是同一个对象(对象内存地址相同),所以List存储了一万个对象引用。但是后者是实打实存储了一万个对象实例,两者的存储空间是质的区别,前者那是相当的小。
  2. 从对象角度来说,前者实际上和存储一个对象没什么区别,因为一万个对象都是一样的(或者你理解为分身术,实体一灭,分身自然就会灭),但是从集合角度来讲,它确实有一万个数量,进行了一万次操作。
  3. 那这和享元模式有什么关系呢?上面添加一万次相同的对象有意义吗?没有,我们肯定不会这么使用,但这个对象实际上就是什么,共享对象,我们稍加改造,就是享元模式的真实使用。
class GlyphContext { // 上下文对象
    private Character flyweight; // 共享的享元对象引用
    private int x, y;           // 外部状态

    public GlyphContext(Character flyweight, int x, int y) {
        this.flyweight = flyweight;
        this.x = x;
        this.y = y;
    }

    public void render() {
        flyweight.display(x, y); // 调用享元方法,传入外部状态
    }
}

// 使用
List<GlyphContext> contexts = new ArrayList<>();
Character sharedCharA = factory.getCharacter('A', "Arial");

for (int i = 0; i < 10000; i++) {
    // 创建 10000 个不同的上下文对象,它们共享同一个 'A' 享元
    contexts.add(new GlyphContext(sharedCharA, i*10, i*5));
}

7. 组合模式

#1. 模式简介

组合模式是一种结构型设计模式,用于将对象组织成树形结构来表示"部分-整体"的层次关系,使客户端可以统一处理单个对象和组合对象。

📌 实际上就是我们常说的树结构,就是组合模式,有children和leaf节点

2. 核心思想

  • 用树形结构表示"部分-整体"关系
  • 统一处理叶子对象和组合对象
  • 支持递归组合形成复杂结构

3. 结构组成

3.1 角色定义
角色 说明
Component 抽象构件,声明组合中对象的通用接口
Leaf 叶子构件,表示组合中的叶子节点,没有子节点
Composite 复合构件,存储子部件,实现与子部件相关的操作
3.2 结构图示
// 抽象构件
interface Component {
    void operation();
    void add(Component c);
    void remove(Component c);
    Component getChild(int i);
}

// 叶子构件
class Leaf implements Component {
    private String name;
    
    public Leaf(String name) {
        this.name = name;
    }
    
    @Override
    public void operation() {
        System.out.println("叶子" + name + "被访问"); 
    }
    
    // 以下方法叶子构件不需要,但为了保持透明性必须实现
    @Override
    public void add(Component c) {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void remove(Component c) {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public Component getChild(int i) {
        throw new UnsupportedOperationException();
    }
}

// 复合构件
class Composite implements Component {
    private List<Component> children = new ArrayList<>();
    
    @Override
    public void operation() {
        for(Component child : children) {
            child.operation();
        }
    }
    
    @Override
    public void add(Component c) {
        children.add(c);
    }
    
    @Override
    public void remove(Component c) {
        children.remove(c);
    }
    
    @Override
    public Component getChild(int i) {
        return children.get(i);
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Component root = new Composite();
        
        Component branch1 = new Composite();
        branch1.add(new Leaf("1"));
        branch1.add(new Leaf("2"));
        
        Component branch2 = new Composite();
        branch2.add(new Leaf("3"));
        
        root.add(branch1);
        root.add(branch2);
        
        root.operation();
    }
}

优点 ✅

  • 简化客户端代码:可以统一处理单个对象和组合对象
  • 易于扩展:新增构件类型容易,符合开闭原则
  • 灵活的结构:可以创建任意复杂的树形结构
  • 统一接口:定义了包含基本对象和组合对象的类层次结构

缺点 ❌

  • 透明性带来的问题:叶子构件需要实现不需要的方法(透明模式)
  • 类型检查问题:可能需要运行时类型检查来确定组件类型
  • 设计复杂性增加:需要仔细设计接口,确保正确性
  • 性能考虑:递归遍历可能影响性能

适用场景

  • 需要表示对象的"部分-整体"层次结构
  • 希望用户忽略组合对象与单个对象的不同
  • 结构可以形成任意复杂的树形结构
  • 需要对树形结构的所有节点执行统一操作

典型应用案例

  1. 文件系统

    • 文件(叶子)和文件夹(组合)的层次结构
  2. GUI组件

    • 窗口包含面板,面板包含按钮等UI元素
  3. 组织结构图

    • 公司包含部门,部门包含员工
  4. 菜单系统

    • 菜单可以包含子菜单和菜单项
  5. 文档处理

    • XML/HTML文档中元素可以包含子元素

与其他设计模式的关系

关系模式 关联说明
装饰器模式 相似点:都使用递归组合
不同点:装饰器只有一个子组件
迭代器模式 组合模式常与迭代器模式一起使用来遍历组合结构
访问者模式 可用于对组合结构中的对象执行操作
享元模式 可以共享叶子节点来优化内存使用
责任链模式 组合结构可以看作是一种责任链
命令模式 组合模式可以用于构建宏命令(组合命令)

六、行为型模式

📌 核心:关注对象间的通信和职责分配,定义对象如何交互和分配任务!

1. 模板方法(Template Method)

定义

📌 它在超类中定义了一个算法的骨架,将算法中的某些步骤延迟到子类中实现,使得子类可以在不改变算法整体结构的情况下,重新定义算法中的某些特定步骤。

📌 简单来说,就是 “定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

核心思想

  • 封装不变部分,扩展可变结构:将通用的部分封装在父类中,可变的部分在子类中实现,增加扩展性。
  • 反向控制结构(父类控制整体实现流程):子类实现个性化部分后不需要调用和执行,由父类进行方法的执行,父类控制整体方法的调用。

角色

1. 抽象类/模板类(Abstract Class)
  • 模板方法(Template Method)

    • 定义了算法的骨架
    • 通常声明为 final(在Java等语言中),防止子类重写整个算法结构
    • 内部按顺序调用其他基本方法
  • 基本方法

    • 抽象方法(Abstract Methods)

      • 由抽象类声明
      • 必须由子类实现
      • 算法中可变的步骤
    • 具体方法(Concrete Methods)

      • 由抽象类声明并实现
      • 算法中通用的、不变的步骤
      • 子类可以继承或重写(通常不鼓励重写)
    • 钩子方法(Hook Methods)

      • 由抽象类声明并提供空或默认的实现
      • 子类可以选择性地重写
      • 在算法的特定点施加影响
      • 不是必须实现的,提供额外扩展点
2. 具体子类(Concrete Class)
  • 继承自抽象类
  • 必须实现抽象类中声明的所有抽象方法
  • 可以选择重写钩子方法

例子(饮料制作)

  1. 抽象类 - 饮料模板(BeverageTemplate)
// 抽象模板类
public abstract class BeverageTemplate {

    // 模板方法:声明为final,防止子类重写算法骨架
    public final void prepareRecipe() {
        boilWater();       // 不变步骤:烧水
        brew();            // 可变步骤:冲泡 -> 抽象方法
        pourInCup();       // 不变步骤:倒入杯中
        if (customerWantsCondiments()) { // 钩子方法:是否加调料
            addCondiments();   // 可变步骤:加调料 -> 抽象方法
        }
    }

    // 抽象方法,必须由子类实现
    protected abstract void brew();
    protected abstract void addCondiments();

    // 具体方法,通用不变步骤
    private void boilWater() {
        System.out.println("将水煮沸");
    }

    private void pourInCup() {
        System.out.println("将饮料倒入杯中");
    }

    // 钩子方法:提供一个默认实现。子类可以视情况选择是否覆盖
    protected boolean customerWantsCondiments() {
        return true; // 默认需要加调料
    }
}
  1. 具体子类 - 咖啡(Coffee)
// 具体子类
public class Coffee extends BeverageTemplate {

    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡粉");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加入糖和牛奶");
    }

    // 覆盖钩子方法:提供自定义逻辑
    @Override
    protected boolean customerWantsCondiments() {
        // 可以从用户输入或其他配置获取答案,这里简单返回false
        String answer = getUserInput(); // 假设这个方法获取用户输入
        return answer.toLowerCase().startsWith("y");
    }

    private String getUserInput() {
        // 模拟获取用户输入的逻辑
        return "yes"; // 或者 "no"
    }
}
  1. 具体子类 - 茶(Tea)
// 具体子类
public class Tea extends BeverageTemplate {

    @Override
    protected void brew() {
        System.out.println("用沸水浸泡茶叶");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加入柠檬");
    }

    // 不覆盖customerWantsCondiments钩子方法,使用父类的默认实现(true)
}
  1. 客户端使用
public class Client {
    public static void main(String[] args) {
        System.out.println("制作咖啡...");
        BeverageTemplate coffee = new Coffee();
        coffee.prepareRecipe(); // 调用模板方法

        System.out.println("\n制作茶...");
        BeverageTemplate tea = new Tea();
        tea.prepareRecipe(); // 调用模板方法
    }
}

优缺点

优点
  1. 提高代码复用性:将相同代码放在父类中,避免了子类中的代码重复。

  2. 良好的扩展性:增加新的具体子类很容易,只需要实现抽象的步骤即可,符合“开闭原则”

  3. 便于维护:算法结构集中在一个地方,修改算法只需修改模板类,而不需要改动所有子类。

  4. 反向控制:通过父类调用子类的操作,实现了反向控制,是框架设计的常用模式。

缺点:
  1. 类数目增加:每个不同的实现都需要一个新的子类,导致系统中类的个数增加,系统更加庞大。

  2. 算法骨架不易变化:由于模板方法被声明为 final,算法骨架难以改变,限制了继承的灵活性。

应用场景

  • 一次性实现算法的不变部分,将可变的行为留给子类实现。

  • 各子类中公共的行为应被提取出来并集中到一个公共父类中,以避免代码重复。

  • 框架设计:这是模板方法模式最经典的应用。框架定义流程骨架,用户通过继承框架类并实现抽象方法,即可将自定义逻辑嵌入框架的特定步骤中。

2. 策略模式

一、定义

📌 定义一系列的算法,并进行封装,并可以相互替换(替换的意思是同一个目的,可以使用不同的算法进行实现),这样客户端可以根据需求选择不同的算法。

📌 核心思想

  • 分离出变与不变:变指的是不同的算法,不变是稳定的上下文,也就是你的需求和目的
  • 面相接口编程:这里的面相接口不是说我们需要实现某一个接口进行具体的实现,而是直接通过接口实现需求
  • 组合优于继承:这里的组合指的是通过将不同的算法按照特定的逻辑进行顺序调用,实现我们的需求,而不是说将这个特定的逻辑在父类中进行封装(这里和模板方法进行区分,模板方法是将同样的逻辑进行封装,由超类进行调用)。

二、角色和职责

1. 策略 (Strategy)
  • 定义:这是一个接口或者抽象类,定义了所有策略需要实现的算法,这个接口是连接上下文和具体策略的桥梁。
  • 职责:申明共有的方法。
2. 具体策略(ConcreteStrategy)
  • 定义:实现了策略的一个具体类,对所有方法进行重写,提供特定的算法。
  • 职责:封装具体的算法。
3. 上下文(Context)
  • 定义:一个特殊的类,是策略模式的核心,拥有某一个策略类,也是和客户端直接交互的类,你可以理解为了解各种产品的经理,他给客户提供不同的产品,但是选择权在客户。
  • 职责:配置策略,也就是提供合适的算法,然后提供给客户进行选择并调用。

三、代码示例(支付场景)

第1步:定义策略接口 (Strategy)

/**
 * 支付策略接口
 */
public interface PaymentStrategy {
    /**
     * 计算支付手续费
     * @param amount 订单金额
     * @return 计算出的手续费
     */
    BigDecimal calculateFee(BigDecimal amount);

    /**
     * 获取支付方式名称
     * @return 名称
     */
    String getPaymentType();
}

第2步:实现具体策略类 (ConcreteStrategy)

/**
 * 支付宝支付策略
 */
public class AlipayStrategy implements PaymentStrategy {
    @Override
    public BigDecimal calculateFee(BigDecimal amount) {
        // 支付宝手续费率: 0.3%
        return amount.multiply(new BigDecimal("0.003"));
    }

    @Override
    public String getPaymentType() {
        return "ALIPAY";
    }
}

/**
 * 微信支付策略
 */
public class WechatPayStrategy implements PaymentStrategy {
    @Override
    public BigDecimal calculateFee(BigDecimal amount) {
        // 微信支付手续费率: 0.28%
        return amount.multiply(new BigDecimal("0.0028"));
    }

    @Override
    public String getPaymentType() {
        return "WECHAT_PAY";
    }
}

/**
 * 信用卡支付策略(可能有最低手续费)
 */
public class CreditCardStrategy implements PaymentStrategy {
    private static final BigDecimal MIN_FEE = new BigDecimal("1.00");
    private static final BigDecimal RATE = new BigDecimal("0.006");

    @Override
    public BigDecimal calculateFee(BigDecimal amount) {
        BigDecimal fee = amount.multiply(RATE);
        // 手续费不足1元按1元收取
        return fee.compareTo(MIN_FEE) < 0 ? MIN_FEE : fee;
    }

    @Override
    public String getPaymentType() {
        return "CREDIT_CARD";
    }
}

第3步:构建上下文类 (Context)

/**
 * 支付上下文
 */
public class PaymentContext {
    // 持有策略对象的引用(组合)
    private PaymentStrategy strategy;

    // 通过构造器注入策略
    public PaymentContext(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    // 通过Setter方法注入策略(支持运行时动态切换)
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 上下文的核心方法,执行支付手续费计算
     * 客户端只需调用此方法,无需关心具体策略
     * @param amount 金额
     * @return 手续费结果
     */
    public BigDecimal executeCalculation(BigDecimal amount) {
        if (strategy == null) {
            throw new IllegalStateException("Payment strategy has not been set.");
        }
        System.out.println("正在使用[" + strategy.getPaymentType() + "]计算手续费...");
        BigDecimal fee = strategy.calculateFee(amount);
        System.out.println("计算完成,手续费为: ¥" + fee.setScale(2, RoundingMode.HALF_UP));
        return fee;
    }
}

第4步:客户端使用

public class Client {
    public static void main(String[] args) {
        BigDecimal orderAmount = new BigDecimal("100.00");

        // 1. 用户选择支付宝支付
        PaymentContext context = new PaymentContext(new AlipayStrategy());
        context.executeCalculation(orderAmount);

        // 2. 运行时动态切换到微信支付(利用了setter)
        context.setStrategy(new WechatPayStrategy());
        context.executeCalculation(orderAmount);

        // 3. 使用信用卡支付
        context.setStrategy(new CreditCardStrategy());
        context.executeCalculation(new BigDecimal("50.00")); // 金额小,测试最低手续费

        // 输出:
        // 正在使用[ALIPAY]计算手续费...
        // 计算完成,手续费为: ¥0.30
        // 正在使用[WECHAT_PAY]计算手续费...
        // 计算完成,手续费为: ¥0.28
        // 正在使用[CREDIT_CARD]计算手续费...
        // 计算完成,手续费为: ¥1.00
    }
}

四、优缺点

  • 优点
    1. 符合开闭原则:无需修改策略类和上下文类,新增策略即可,由客户端进行选择,按照既定的上下文运行。
    2. 将复杂的判断逻辑从上下文类和客户端移除:因为选择权在客户端,客户端一般需要了解不同的策略和优劣。
    3. 算法复用和灵活性:不同的上下文共享一个策略(正常情况下可以),而且可以灵活切换,比如选择不同的支付方式。
  • 缺点:
    • 客户端必须了解所有策略
    • 对象数量增加:如果策略太多,那类的数量就会变多
    • 通信开销:存在A策略不需要B策略的方法,但是都需要实现,解决方案:可以使用适配器模式 (Adapter Pattern) 或让上下文提供默认数据。(注意:这里的两种方法大家可以自行百度,有一些复杂在这里就不展示了,只需要明白一点,适配器适用于旧系统的改造,上下文默认数据适用于在旧的基础上在扩展新功能

五、典型应用场景

  1. 支付系统:如本文示例,不同的支付方式、优惠券计算规则、国际货币转换等。

  2. 导航系统:不同的路线规划算法(最快、最短、避开收费、避开高速)。

  3. 游戏开发:游戏中NPC或角色的不同AI行为(攻击、逃跑、巡逻)。

  4. 数据验证:针对不同类型的数据(邮箱、手机号、身份证号)采用不同的验证策略。

  5. 排序算法:根据需要(速度、稳定性、内存占用)选择不同的排序算法(快速排序、归并排序、冒泡排序)。

Java API 中的应用

  • java.util.Comparator 接口是经典的策略模式。你可以为同一个 Collections.sort() 方法传入不同的 Comparator 策略来实现不同的排序规则。

  • javax.servlet.http.HttpServlet 的 service 方法,根据 HTTP Method (GET, POST) 调用不同的 doGet, doPost 方法,这也是一种策略思想。

3. 命令模式(稍显复杂)

一、概念

官方定义 (GoF):将一个请求封装为一个对象,从而使您可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

核心思想

  1. 解耦请求者和接受者:在请求者和接受者对象增加了一层命令(Command)作为中介,这样请求者不需要知道谁是接受者和具体的细节。
  2. 将请求对象化:通常请求都是直接调用方法,而命令模式是将接受者、请求者、参数、命令等等都封装为一个对象,方便记录和进行其它各种高级操作。
  3. 支持撤销与重做:因为第二步对象化的缘故,所以可以将很多操作进行回滚。

二、角色

1. 命令 (Command)
  • 描述:一个接口,通常只声明一个方法execute(),所有的具体命令必须实现这个接口。
  • 职责:统一了命令的协议,是接受者和发送者的桥梁。
2. 具体命令 (ConcreteCommand)
  • 描述:实现了Command接口的具体类,系统中的核心。
  • 职责
    • 绑定接收者与动作:在其内部持有一个接收者对象 (Receiver) 的引用,并将调用接收者的具体动作

    • 实现 execute():在 execute() 方法中调用接收者的一个或多个动作来完成具体的业务逻辑

    • 实现 undo():(可选)存储执行前的状态,并在 undo() 方法中将接收者的状态恢复。

3. 命令 (Receiver)
  • 描述:知道如何执行与请求相关的操作。任何类都可以作为接收者。
  • 职责:包含实际要执行逻辑的代码,比如打开灯的具体代码。
4. 调用者/触发者 (Invoker)
  • 描述:要求命令对象执行请求。它持有命令对象的引用。
  • 职责
    • 设置命令:通过构造方法或者setter方法接收一个命令对象。

    • 触发执行:在某个时间点或者触发点调用对应的execute命令。

  • 重点:调用者不知道命令的具体内容和接收者,只和命令耦合。
5. 客户端 (Client)
  • 角色:创建具体的命令对象,并设置其接收者。然后将命令对象配置给调用者。

  • 职责:组装命令模式中的各个对象,确定“谁”、“做什么”、“发给谁”。

三、完整代码示例:智能家居遥控器

第1步:定义命令接口 (Command)

/**
 * 命令接口
 */
public interface Command {
    void execute(); // 执行命令
    void undo();    // 撤销命令(可选但强大)
}

第2步:创建接收者 (Receiver)

/**
 * 电灯(接收者)
 */
public class Light {
    private String location;

    public Light(String location) {
        this.location = location;
    }

    public void on() {
        System.out.println(location + "的电灯打开了");
    }

    public void off() {
        System.out.println(location + "的电灯关闭了");
    }
}

/**
 * 音响系统(接收者)
 */
public class Stereo {
    private int volume = 5;

    public void on() {
        System.out.println("音响系统已开启");
    }

    public void off() {
        System.out.println("音响系统已关闭");
    }

    public void setCD() {
        System.out.println("音响已设置为播放CD");
    }

    public void setVolume(int volume) {
        this.volume = volume;
        System.out.println("音响音量设置为: " + volume);
    }

    public int getVolume() {
        return volume;
    }
}

第3步:实现具体命令 (ConcreteCommand)

/**
 * 打开电灯的命令
 */
public class LightOnCommand implements Command {
    private Light light; // 命令的接收者

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on(); // 调用接收者的方法
    }

    @Override
    public void undo() {
        light.off(); // 撤销就是关闭电灯
    }
}

/**
 * 关闭电灯的命令
 */
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void undo() {
        light.on(); // 撤销就是打开电灯
    }
}

/**
 * 打开音响并播放CD的宏命令
 */
public class StereoOnWithCDCommand implements Command {
    private Stereo stereo;
    private int previousVolume; // 用于存储状态,实现撤销

    public StereoOnWithCDCommand(Stereo stereo) {
        this.stereo = stereo;
    }

    @Override
    public void execute() {
        stereo.on();
        stereo.setCD();
        previousVolume = stereo.getVolume(); // 记录之前的音量
        stereo.setVolume(11); // 设置一个不错的音量
    }

    @Override
    public void undo() {
        stereo.setVolume(previousVolume); // 恢复音量
        stereo.off(); // 关闭音响
    }
}

第4步:构建调用者 (Invoker) - 遥控器

/**
 * 遥控器(调用者)
 * 它有7个插槽和1个撤销按钮
 */
public class RemoteControl {
    private Command[] onCommands;
    private Command[] offCommands;
    private Command undoCommand; // 记录最后执行的命令,用于撤销

    public RemoteControl() {
        onCommands = new Command[7];
        offCommands = new Command[7];

        // 初始化所有插槽为空命令,避免null检查
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    /**
     * 设置插槽的命令
     * @param slot 插槽位置
     * @param onCommand 开命令
     * @param offCommand 关命令
     */
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    /**
     * 按下开的按钮
     * @param slot 插槽位置
     */
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot]; // 记录最后执行的命令
    }

    /**
     * 按下关的按钮
     * @param slot 插槽位置
     */
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot]; // 记录最后执行的命令
    }

    /**
     * 按下撤销按钮
     */
    public void undoButtonWasPushed() {
        undoCommand.undo();
    }

    @Override
    public String toString() {
        StringBuilder stringBuff = new StringBuilder();
        stringBuff.append("\n------ 遥控器 ------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[插槽 ").append(i).append("] ")
                    .append(onCommands[i].getClass().getSimpleName())
                    .append("    ")
                    .append(offCommands[i].getClass().getSimpleName())
                    .append("\n");
        }
        stringBuff.append("[撤销] ").append(undoCommand.getClass().getSimpleName()).append("\n");
        return stringBuff.toString();
    }
}

/**
 * 空对象(Null Object),用于设计模式初始化
 */
public class NoCommand implements Command {
    @Override
    public void execute() {
        // 什么都不做
    }

    @Override
    public void undo() {
        // 什么都不做
    }
}

第5步:客户端组装并测试 (Client)

/**
 * 客户端:组装命令模式
 */
public class RemoteLoader {
    public static void main(String[] args) {
        // 1. 创建调用者 - 遥控器
        RemoteControl remoteControl = new RemoteControl();

        // 2. 创建接收者 - 实际设备
        Light livingRoomLight = new Light("客厅");
        Light kitchenLight = new Light("厨房");
        Stereo stereo = new Stereo();

        // 3. 创建具体命令,并绑定接收者
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);

        // 4. 将命令加载到调用者的插槽中
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, stereoOnWithCD, new NoCommand()); // 只有一个命令

        System.out.println(remoteControl);

        // 5. 模拟按钮按下
        remoteControl.onButtonWasPushed(0); // 打开客厅灯
        remoteControl.offButtonWasPushed(0); // 关闭客厅灯
        remoteControl.undoButtonWasPushed(); // 撤销!客厅灯又打开了

        remoteControl.onButtonWasPushed(1); // 打开厨房灯
        remoteControl.undoButtonWasPushed(); // 撤销!厨房灯关闭了

        remoteControl.onButtonWasPushed(2); // 打开音响
        remoteControl.undoButtonWasPushed(); // 撤销!音响关闭了
    }
}

四、优缺点

优点:
  • 解耦性 (Decoupling):调用者与接收者完全解耦。调用者不知道也不关心具体执行什么操作以及谁去执行。

  • 可扩展性:添加新命令非常容易,只需创建新的 ConcreteCommand 类,无需修改现有代码,符合开闭原则。

  • 支持高级功能

    • 命令队列 (Queue):可以将命令对象放入队列中,实现延迟执行、异步执行或任务调度。

    • 日志与持久化:可以记录执行过的命令对象序列,用于实现审计日志、事务系统或崩溃恢复。通过序列化命令,甚至可以存储到磁盘。

    • 宏命令 (Macro Command):可以创建一种命令,它是其他命令的集合,用于一次性执行一系列操作。

    • 撤销/重做 (Undo/Redo):这是命令模式最著名的应用。

缺点:
  • 类的数量爆炸:每个操作都需要一个具体的命令类,可能会导致系统中类的数量大量增加,增加系统复杂性。

  • 复杂度增加:对于简单的操作,使用命令模式可能会显得“杀鸡用牛刀”,使代码变得不直观。

五、典型应用场景

  • GUI 按钮和菜单项:每个按钮点击或菜单选择都被绑定到一个命令对象。

  • 事务系统:数据库中的事务操作。如果事务失败,可以执行一系列撤销命令。

  • 任务调度器/线程池:将任务封装为命令对象,提交给执行器异步执行。

  • 宏录制功能:记录用户的操作序列(一系列命令),然后可以回放。

  • 网络通信:将请求封装为命令对象,序列化后通过网络发送,在另一端执行。

六、如何理解命令模式

    命令模式是一种非常复杂的设计模式,常常用于构建一个大型的系统或者模块,而且功能全面,稳定,有非常良好的性能,最大的优点就是可以撤销。其实在正常的开发中我们是用不到的,这往往需要非常多的经验和强大的架构和逻辑能力,这里我们做了解就好。

4. 职责链(责任链)模式 ——重点!!!

    职责链模式之所以在这里是重点是因为应用的很广泛,这个应用大多数在各种技术的底层,是一种非常重要的思想,比如java Servlet的FilterspringMvc的dispatchServlet中也应用了这种思想,这对我们后面的面试和技术的了解非常重要。

一、核心思想

    解耦请求的发送者和接收者,让多个接收者都有机会处理这个请求,这些接收者组成一个链,请求沿着这个链传递,直到有一个对象处理这个请求。

📌 比喻:员工发起一个请假的流程或者提交一个审批的单子,他不需要指定谁进行审批,也不需要知道这个单子会在内部怎么流转,由系统处理,最后交给员工一个结果。

📌 核心:系统构建一个审批的链条,根据员工的提交内容进行判断谁进行审批,下一个节点是谁,最终返回一个结果。

  1. 请求:报销单。

  2. 职责链:员工 -> 部门经理 -> 总监 -> CFO -> CEO

  3. 处理规则

  • 部门经理只能审批金额 <= 1000 元的单子。

  • 总监能审批金额 <= 5000 元的单子。

  • CFO能审批金额 <= 10000 元的单子。

  • CEO可以审批任何金额的单子。

  1. 流程:员工提交报销单(发出请求)。报销单首先送到部门经理那里。如果金额在经理权限内,经理处理(批准或拒绝),流程结束。如果超出权限,经理无权处理,他会将单子传递给下一环——总监。总监重复同样的判断过程,以此类推,直到单子被某位领导处理为止。

二、要解决的核心问题

  1. 紧耦合:请求发送者类必须知道所有潜在的处理者类,并包含复杂的条件判断逻辑来决定将请求发给谁。
// 紧耦合的代码:发送者需要了解所有处理者
// 添加一个新的处理者 handlerD 时,必须修改这段发送者的代码,违反了开闭原则 
if (request.getType() == "A") {
    handlerA.handle(request);
} else if (request.getType() == "B") {
    handlerB.handle(request);
} else if (request.getType() == "C") {
    handlerC.handle(request);
} else {
    // 默认处理...
}
  1. 职责不单一:发送者不仅要处理自己的需求,还需要知道被处理之间的关系。
  2. 不灵活:处理流程被硬编码,难以在运行时动态地改变处理顺序或增加、移除处理者。

三、主要角色

  1. 抽象处理者(Handler)
  • 定义:声明了一个处理请求的接口。通常是一个抽象类或接口。

  • 关键:它包含一个指向下一个处理者 (next handler) 的链接(通常通过一个成员变量,如 protected Handler successor;),并提供了一个方法(如 setSuccessor)来设置链中的下一个处理者

  • 核心方法:声明了 handleRequest 方法,用于处理请求或将其转发给下一个处理者。

  1. 具体处理者
  • 定义:实现处理请求的具体逻辑。

  • 关键:当收到请求时,具体处理者可以选择自行处理请求,或者将请求传递给链中的下一个处理者。这个决定通常基于请求的某些属性(如类型、金额等)。

协作流程

  • 客户端创建职责链,将多个具体处理者对象链接起来(aHandler.setSuccessor(bHandler))。

  • 客户端向链的头部发起请求,无需关心具体由谁处理。

  • 请求在链中传递,直到某个具体处理者处理了它,或者到达链的末端。

四、代码示例

第1步:定义抽象处理者 (Handler)

/**
 * 抽象处理者:定义处理请求的接口和链结构
 */
public abstract class Approver {
    // 关键:持有下一个处理者的引用
    protected Approver successor;

    // 设置下一个处理者(用于构建链)
    public void setSuccessor(Approver successor) {
        this.successor = successor;
    }

    // 处理请求的抽象方法
    public abstract void processRequest(PurchaseRequest request);
}

第2步:定义请求类 (Request)

/**
 * 请求类:封装请求的详细信息
 */
public class PurchaseRequest {
    private String purpose; // 用途
    private double amount;  // 金额

    public PurchaseRequest(String purpose, double amount) {
        this.purpose = purpose;
        this.amount = amount;
    }

    public double getAmount() {
        return this.amount;
    }
    // 省略其他getter...
}

第3步:创建具体处理者 (Concrete Handlers)

/**
 * 具体处理者:部门经理
 */
public class Manager extends Approver {

    public Manager(String name) {
        super(name);
    }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() <= 5000) { // 经理有权审批5000元以内的开支
            System.out.println("经理 " + this.name + " 审批了采购请求:[" + request.getPurpose() + "],金额:" + request.getAmount() + "元");
        } else if (successor != null) { // 如果金额超出权限,并且有下一个处理者
            System.out.println("经理 " + this.name + " 无权限审批,转交上级...");
            successor.processRequest(request); // 传递给下一环
        }
        // 如果没有后继者,链到此结束,请求未被处理(可根据需要添加默认行为)
    }
}

/**
 * 具体处理者:总监
 */
public class Director extends Approver {

    public Director(String name) {
        super(name);
    }

    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() <= 10000) { // 总监有权审批10000元以内的开支
            System.out.println("总监 " + this.name + " 审批了采购请求:[" + request.getPurpose() + "],金额:" + request.getAmount() + "元");
        } else if (successor != null) {
            System.out.println("总监 " + this.name + " 无权限审批,转交上级...");
            successor.processRequest(request);
        }
    }
}

/**
 * 具体处理者:CEO(链的末端)
 */
public class CEO extends Approver {

    public CEO(String name) {
        super(name);
    }

    // CEO作为链的最后一环,通常处理所有其他环节无法处理的请求
    @Override
    public void processRequest(PurchaseRequest request) {
        if (request.getAmount() <= 50000) {
            System.out.println("CEO " + this.name + " 审批了采购请求:[" + request.getPurpose() + "],金额:" + request.getAmount() + "元");
        } else {
            System.out.println("金额过大,需要召开董事会决定!请求:[" + request.getPurpose() + "],金额:" + request.getAmount() + "元");
        }
        // CEO没有successor,链到此绝对终止
    }
}

第4步:客户端构建链并发送请求

注意: 下面的客户端示例其实还是出现了一部分接受者的逻辑,但是在实际使用中这部分还是会进行封装,会使用到FactoryBean工厂等等,这里只是简单的理解责任链的核心。

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        // 1. 创建处理者(审批人)
        Approver manager = new Manager("张经理");
        Approver director = new Director("李总监");
        Approver ceo = new CEO("王总裁");

        // 2. 构建职责链:经理 -> 总监 -> CEO
        manager.setSuccessor(director);
        director.setSuccessor(ceo);
        // 注意:CEO是最后一环,不需要设置successor

        // 3. 创建不同的请求
        PurchaseRequest request1 = new PurchaseRequest("购买办公用品", 4500);
        PurchaseRequest request2 = new PurchaseRequest("团队建设活动", 12000);
        PurchaseRequest request3 = new PurchaseRequest("新服务器采购", 80000);

        // 4. 将请求发送到链的头部,无需关心具体处理者
        System.out.println(">>> 处理请求1:");
        manager.processRequest(request1); // 应由经理处理

        System.out.println("\n>>> 处理请求2:");
        manager.processRequest(request2); // 经理 -> 总监 -> CEO? 实际由总监处理

        System.out.println("\n>>> 处理请求3:");
        manager.processRequest(request3); // 经理 -> 总监 -> CEO -> 董事会
    }
}

五、 模式的优势与适用场景

优势:
  1. 降低耦合度:发送者无需知道请求由谁处理,处理者也无需知道链的全貌,二者解耦。

  2. 增强灵活性:可以动态地增加、删除或修改链中的处理者顺序,符合开闭原则。

  3. 简化对象:每个处理者只需保持一个后继者的引用,结构简单。

  4. 职责清晰:每个处理者只处理自己职责范围内的请求,符合单一职责原则。

六、 潜在的缺点与变体

缺点:
  1. 请求不一定被处理:请求可能到达链的末端也无法被处理,因此需要有良好的默认或错误处理机制。

  2. 性能影响:如果链过长,请求的传递可能会带来性能开销。不适合对性能要求极高的场景。

  3. 调试困难:请求的传递逻辑是分布式的,调试时可能难以跟踪请求的传递路径。

常见变体:
  1. 短路处理:一旦某个处理者处理了请求,立即终止传递(如上面的例子)。

  2. 广播式:请求在链中传递,每个处理者都对其进行某种操作,但不一定终止传递(如Servlet Filter链,每个Filter都对Request和Response进行操作)。

  3. 功能链:每个处理者对请求进行一部分处理,然后传递给下一个,最终组合出结果。

七、接近真实开发的责任链FactoryBean工厂应用示例

// 1. 使用Spring的@Component注解声明各个处理者为Bean
@Component
@Order(1) // 使用Order注解或自定义注解定义顺序
public class Manager extends Approver { ... }

@Component
@Order(2)
public class Director extends Approver { ... }

@Component
@Order(3)
public class CEO extends Approver { ... }

// 2. 创建一个链的配置类或工厂,自动注入所有Approver并排序组装
@Configuration
public class ApprovalChainConfig {

    // 注入所有实现了Approver接口的Bean,Spring会自动将它们收集到List中
    @Autowired
    private List<Approver> approvers;

    @Bean
    public Approver approvalChain() {
        // 对处理者进行排序(例如根据@Order注解)
        approvers.sort(Comparator.comparing(Approver::getOrder));
        
        // 构建责任链
        for (int i = 0; i < approvers.size() - 1; i++) {
            approvers.get(i).setSuccessor(approvers.get(i + 1));
        }
        // 返回链的头节点(第一个处理者)
        return approvers.get(0);
    }
}

// 3. 客户端(如一个Service)直接使用组装好的链
@Service
public class PurchaseService {
    @Autowired // 注入的是已经组装好的完整责任链的头节点
    private Approver approvalChain;

    public void submitRequest(PurchaseRequest request) {
        // 客户端代码非常简洁,完全不知道链的细节
        approvalChain.processRequest(request);
    }
}

八、java中责任链的经典应用

Java Servlet Filter:责任链模式的经典应用
  1. Handler接口:javax.servlet.Filter 接口定义了 doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 方法。这就是处理请求的标准接口。

  2. Concrete Handlers:开发者编写的每一个实现了Filter接口的类,都是一个具体处理者(如LoggingFilter、AuthenticationFilter、EncodingFilter)。

  3. Chain对象:FilterChain 对象是关键!它是由Servlet容器(如Tomcat)在启动时,根据web.xml或@WebFilter注解的配置自动组装而成的责任链。它持有所有Filter的引用和当前执行的位置。

  • 流程:在每个Filter的doFilter方法中,开发者可以选择是否调用 chain.doFilter(request, response)。

  • 调用:意味着将请求和响应对象传递给链中的下一个Filter(或者最终的目标Servlet)。这对应于职责链中的“传递请求”。

  • 不调用:意味着中断链条,直接返回响应(例如,认证失败时直接返回401错误)。这对应于职责链中的“处理请求并终止传递”。

Spring MVC的DispatcherServlet

DispatcherServlet本身不是责任链模式的应用。它是一个前端控制器(Front Controller) 模式的标准实现。

它的职责:作为所有HTTP请求的统一入口,负责协调和委托工作给其他组件(即所谓的“调度”),但它自身并不构成一个可传递的责任链。

它内部使用了责任链模式:DispatcherServlet委托工作的过程,大量使用了责任链模式的思想:

  • HandlerMapping:找出处理请求的Controller。可以配置多个HandlerMapping,它们会按顺序尝试匹配,形成一个隐式的责任链。

  • HandlerAdapter:真正执行Controller方法。多个Adapter形成一个链,直到找到一个能适配当前Controller的为止。

  • HandlerExceptionResolver:解析异常。配置多个Resolver,形成一个标准的责任链,逐个尝试,直到有一个能处理当前异常为止。

工作流/消息队列(MQ)

📌 工作流和MQ来说是一种思想,这要比责任链模式复杂得多,但是核心思想都是高度重合的。

工作流引擎:一个业务流程(BPMN)本质上就是一条预定义好的、可视化的责任链。每个节点(审批任务、自动任务、网关)都是链上的一个处理者。引擎的核心就是驱动一个数据(流程实例)沿着这条定义好的链进行流转。这与责任链模式的理念高度一致。

消息队列

  • 概念上:消息队列本身更像一个通道,它解耦了消息生产者(发送者)和消费者(处理者)。生产者不知道最终是哪个消费者处理消息。

  • 模式上:常见的管道过滤器(Pipes and Filters) 架构模式,就是由MQ连接起多个Filter(消费者),每个Filter对消息进行处理后,再将其放回队列传递给下一个Filter。这可以看作是分布式、松散耦合的责任链的一种实现。每个消费者都是链上的一个环节。

5. 状态模式

一、 核心思想

核心思想:将对象的状态封装成独立的类,并将对象的行为委托给代表当前状态的对象。

注意:这个不太好懂,核心就两个字“委托”,也就是将一些判断逻辑封装而已,根据例子一下就懂了

举例:自动售货机
  1. 无硬币状态 (NoCoinState):
  • 行为:投入硬币 -> 切换到“有硬币”状态;选择饮料 -> 提示“请先投币”。
  1. 有硬币状态 (HasCoinState):
  • 行为:投入硬币 -> 退币(因为已投过);选择饮料 -> 出货,并根据库存切换到“售出”或“售罄”状态。
  1. 售出状态 (SoldState):
  • 行为:执行出货操作,然后自动检查库存。如果还有库存,切换到“无硬币”状态;如果没库存了,切换到“售罄”状态。
  1. 售罄状态 (SoldOutState):
  • 行为:投入硬币 -> 退币;选择饮料 -> 提示“商品已售罄”。

模式的精髓

  • 售货机(上下文)本身并不用一堆 if-else 或 switch-case 来判断自己该做什么

  • 它只是持有一个代表当前状态的对象(如HasCoinState)。

  • 当发生一个动作(如insertCoin())时,售货机 simply 将这个动作委托给当前状态对象来处理:currentState.insertCoin()。

  • 状态对象自己知道如何处理这个动作,包括执行操作和决定下一个状态是什么。

这样做的好处是:将复杂的状态判断逻辑分散到各个状态类中,使得每个类的职责都变得单一且清晰,大大降低了维护成本。

二、错误代码示例

public class VendingMachine {
    // 用常量表示状态
    final static int NO_COIN = 0;
    final static int HAS_COIN = 1;
    final static int SOLD = 2;
    final static int SOLD_OUT = 3;

    private int currentState = SOLD_OUT;
    private int count; // 库存数量

    public VendingMachine(int count) {
        this.count = count;
        if (count > 0) {
            currentState = NO_COIN;
        }
    }

    // 投入硬币
    public void insertCoin() {
        switch (currentState) {
            case NO_COIN:
                currentState = HAS_COIN;
                System.out.println("您投入了一枚硬币。");
                break;
            case HAS_COIN:
                System.out.println("已经投入过硬币,无需再投。");
                break;
            case SOLD:
                System.out.println("请稍候,正在出货...");
                break;
            case SOLD_OUT:
                System.out.println("商品已售罄,无法投币。");
                break;
        }
    }

    // 选择饮料
    public void selectDrink() {
        switch (currentState) {
            case NO_COIN:
                System.out.println("请先投入硬币。");
                break;
            case HAS_COIN:
                System.out.println("正在出货...");
                currentState = SOLD;
                dispense(); // 出货
                break;
            case SOLD:
                System.out.println("正在出货中,请勿重复选择。");
                break;
            case SOLD_OUT:
                System.out.println("商品已售罄。");
                break;
        }
    }

    // 出货
    public void dispense() {
        switch (currentState) {
            case NO_COIN:
            case HAS_COIN:
            case SOLD_OUT:
                System.out.println("发生错误。");
                break;
            case SOLD:
                count--;
                System.out.println("商品已推出,请取用。");
                if (count > 0) {
                    currentState = NO_COIN;
                } else {
                    currentState = SOLD_OUT;
                }
                break;
        }
    }
    // ... 其他方法,如 ejectCoin() 退币,同样需要庞大的switch
}

这种代码的弊端

  • 臃肿且难以维护:每个方法都变成了一个庞大的条件语句。添加一个新状态(如“故障状态”)需要修改每一个方法,违反开闭原则。

  • 可读性差:所有状态的逻辑都混杂在一起,难以阅读和理解。

  • 容易出错:状态转换(currentState = XXX)散布在各个方法的 case 语句中,难以保证所有状态转换都是正确的。

  • 职责不单一:VendingMachine 类承担了所有状态下的所有行为,过于复杂。

注意有的人可能会觉得这些无所谓,比如我的代码中就有三四种判断,根据不同的状态我写几种逻辑其实也还行,为了方便就不会做这种处理。但实际开发中这种修改是有很大弊端的,首先你确实更改了核心的业务逻辑,一旦新增的功能或者业务加入,测试是需要将整个流程走一遍,不但消耗时间,而且一旦出现问题,之前的业务场景就有可能破坏。如果你遇到了一个还行的测试可能不会说什么,但是有的测试可真会骂人的,尤其是那种会写代码的测试,千万不要小瞧了

三、角色分类

1. 上下文
  • 定义:就是拥有状态的对象,比如上面的VendingMachine类。

  • 职责

    • 维护一个ConcreteState子类的实例,这个实例定义当前状态。

    • 将状态相关的请求委托给当前的状态对象处理。

    • 提供方法(如setState)供状态对象来更新它的状态。

2. 状态接口
  • 定义:一个接口或抽象类,定义了所有具体状态类需要实现的方法。这些方法通常对应着上下文对象中那些受状态影响的行为。
3. 具体状态
  • 定义:实现了State接口的类。每个类封装了一个特定状态下的行为。

  • 职责

    • 实现本状态下的行为逻辑。

    • 在需要时,持有一个对Context对象的引用,以便在处理完动作后能够调用Context的setState方法来切换到下一个状态。

四、 代码实现示例(Java - 售货机案例)

第1步:定义状态接口 (State)

/**
 * 状态接口:定义了售货机所有可能的行为
 */
public interface State {
    void insertCoin(); // 投币
    void ejectCoin();  // 退币
    void selectDrink(); // 选择饮料
    void dispense();    // 出货
}

第2步:创建具体状态类 (ConcreteState)

/**
 * 具体状态:有硬币状态
 */
public class HasCoinState implements State {
    private VendingMachine vendingMachine;

    public HasCoinState(VendingMachine vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertCoin() {
        System.out.println("已经投入过硬币,无需再投。");
    }

    @Override
    public void ejectCoin() {
        System.out.println("硬币退回。");
        // 重要:操作完成后,将售货机状态设置为“无硬币”
        vendingMachine.setState(vendingMachine.getNoCoinState());
    }

    @Override
    public void selectDrink() {
        System.out.println("您选择了饮料...");
        // 重要:切换到“售出”状态
        vendingMachine.setState(vendingMachine.getSoldState());
    }

    @Override
    public void dispense() {
        System.out.println("尚未选择饮料,无法出货。");
    }
}

/**
 * 具体状态:售出状态
 */
public class SoldState implements State {
    private VendingMachine vendingMachine;

    public SoldState(VendingMachine vendingMachine) {
        this.vendingMachine = vendingMachine;
    }

    @Override
    public void insertCoin() {
        System.out.println("请稍候,正在出货...");
    }

    @Override
    public void ejectCoin() {
        System.out.println("抱歉,您已经选择了商品,无法退币。");
    }

    @Override
    public void selectDrink() {
        System.out.println("正在出货中,请勿重复选择。");
    }

    @Override
    public void dispense() {
        vendingMachine.releaseDrink(); // 实际出货
        if (vendingMachine.getCount() > 0) {
            // 如果还有库存,回到“无硬币”状态
            vendingMachine.setState(vendingMachine.getNoCoinState());
        } else {
            // 如果没库存了,切换到“售罄”状态
            System.out.println("通知:商品已售罄。");
            vendingMachine.setState(vendingMachine.getSoldOutState());
        }
    }
}
// 类似地,实现 NoCoinState 和 SoldOutState (篇幅所限,省略详细代码)

第3步:重构上下文类 (Context - VendingMachine)

/**
 * 上下文:售货机
 */
public class VendingMachine {
    // 持有所有状态对象的引用
    private State noCoinState;
    private State hasCoinState;
    private State soldState;
    private State soldOutState;

    private State currentState; // 当前状态
    private int count = 0; // 库存

    public VendingMachine(int numberDrinks) {
        // 初始化所有状态对象,将自身(this)传递进去
        noCoinState = new NoCoinState(this);
        hasCoinState = new HasCoinState(this);
        soldState = new SoldState(this);
        soldOutState = new SoldOutState(this);

        this.count = numberDrinks;
        if (numberDrinks > 0) {
            currentState = noCoinState; // 初始状态
        } else {
            currentState = soldOutState;
        }
    }

    // 将行为委托给当前状态对象!
    public void insertCoin() {
        currentState.insertCoin();
    }
    public void ejectCoin() {
        currentState.ejectCoin();
    }
    public void selectDrink() {
        currentState.selectDrink();
        currentState.dispense(); // 选择后通常紧接着出货
    }

    // 实际出货方法,内部调用,不属于状态接口
    public void releaseDrink() {
        System.out.println("一瓶饮料正在推出...");
        if (count != 0) {
            count = count - 1;
        }
    }

    // Getter for states and count
    public State getNoCoinState() { return noCoinState; }
    public State getHasCoinState() { return hasCoinState; }
    public State getSoldState() { return soldState; }
    public State getSoldOutState() { return soldOutState; }
    public int getCount() { return count; }

    // Setter for state (非常重要,供状态对象调用)
    public void setState(State state) {
        this.currentState = state;
    }
}

第4步:客户端使用

public class Client {
    public static void main(String[] args) {
        VendingMachine machine = new VendingMachine(2); // 装入2瓶饮料

        System.out.println("--- 测试1: 正常购买 ---");
        machine.insertCoin();
        machine.selectDrink(); // 购买第一瓶

        System.out.println("\n--- 测试2: 再次购买 ---");
        machine.insertCoin();
        machine.selectDrink(); // 购买第二瓶,此时会售罄

        System.out.println("\n--- 测试3: 售罄后尝试 ---");
        machine.insertCoin(); // 此时应提示售罄
    }
}

五、 模式的优势与适用场景

优势:
  • 单一职责原则:将与特定状态相关的代码组织到独立的类中,代码清晰。

  • 开闭原则:要引入新状态,只需添加新的状态类,无需修改现有状态类或上下文(大部分情况下),易于扩展。

  • 消除庞大的条件语句:将复杂的状态判断逻辑分布到各个状态类中,使代码更易于维护和理解。

  • 使状态转换显式化:状态转换的规则被集中到了状态对象中,变得更加明确。

适用场景:
  • 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为。

  • 一个操作中含有庞大的多分支条件语句,且这些分支依赖于对象的状态(例如前文中的switch例子)。出现这种情况时,状态模式就是你的救命稻草。

常见应用:
  • 工作流引擎(如订单状态:待支付、已支付、已发货、已完成、已取消)。

  • 游戏开发(如角色状态:站立、行走、奔跑、跳跃、攻击)。

  • TCP网络连接(状态:连接已建立、正在监听、连接已关闭)。

  • UI控件(按钮状态:正常、悬停、点击、禁用)。

六、思考两种情况

思考1:

问题:

有稍微多的判断语句,但是不同判断下执行的逻辑都是一样的,不同点是不同的类型调用的方法不一样,可能会有某些特殊的类型还需要再处理一些个性化逻辑。

方案:

状态模式的核心是状态改变导致行为完全改变。而您描述的场景更像是同一行为的不同算法实现,这更符合策略模式 (Strategy Pattern) 的思想。如果再结合模板方法模式 (Template Method) 来处理共性和个性化逻辑,将是完美的解决方案。

示例:

// 策略接口
public interface PaymentStrategy {
    PaymentResult pay(PaymentRequest request) throws PaymentException;
}

// 抽象的模板策略,处理通用逻辑
public abstract class AbstractPaymentStrategy implements PaymentStrategy {
    // 模板方法:定义了支付的标准流程
    @Override
    public final PaymentResult pay(PaymentRequest request) throws PaymentException {
        // 1. 通用步骤:验证参数(所有支付方式都需要的逻辑)
        validateRequest(request);
        
        // 2. 通用步骤:记录请求日志
        logRequest(request);
        
        // 3. 特定步骤:执行支付(由子类实现)
        PaymentResult result = executePayment(request);
        
        // 4. 通用步骤:记录结果日志
        logResult(result);
        
        // 5. 钩子方法:允许子类在标准流程后做一些个性化事情
        postProcess(result, request);
        
        return result;
    }
    
    // 通用方法,可被所有子类复用
    protected void validateRequest(PaymentRequest request) { ... }
    protected void logRequest(PaymentRequest request) { ... }
    protected void logResult(PaymentResult result) { ... }
    
    // 抽象方法,必须由子类实现
    protected abstract PaymentResult executePayment(PaymentRequest request) throws PaymentException;
    
    // 钩子方法 (Hook),子类可以选择性地覆盖它来做个性化逻辑,不是必须的
    protected void postProcess(PaymentResult result, PaymentRequest request) {
        // 默认空实现
    }
}

// 具体策略:支付宝支付
@Component("ALIPAY")
public class AlipayStrategy extends AbstractPaymentStrategy {
    @Override
    protected PaymentResult executePayment(PaymentRequest request) {
        // 调用支付宝特有的SDK进行支付
        return alipayClient.pay(...);
    }
    
    // 覆盖钩子方法,添加支付宝的个性化逻辑(例如同步通知处理)
    @Override
    protected void postProcess(PaymentResult result, PaymentRequest request) {
        if (result.isSuccess()) {
            // 支付宝特有的后处理逻辑
            sendAlipaySyncNotification(...);
        }
    }
}

// 具体策略:信用卡支付
@Component("CREDIT_CARD")
public class CreditCardStrategy extends AbstractPaymentStrategy {
    @Override
    protected PaymentResult executePayment(PaymentRequest request) {
        // 调用银行网关进行支付
        return bankGateway.charge(...);
    }
    // 不需要个性化后处理,就不覆盖postProcess方法
}

// 策略工厂(用于根据类型获取策略)
@Service
public class PaymentStrategyFactory {
    @Autowired
    private Map<String, PaymentStrategy> strategyMap; // Spring会自动将Bean名和Strategy注入到Map中
    
    public PaymentStrategy getStrategy(String paymentType) {
        PaymentStrategy strategy = strategyMap.get(paymentType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unsupported payment type: " + paymentType);
        }
        return strategy;
    }
}

// 客户端使用
@Service
public class PaymentService {
    @Autowired
    private PaymentStrategyFactory factory;
    
    public PaymentResult handlePayment(PaymentRequest request) {
        // 根据请求中的支付类型,获取对应的策略
        PaymentStrategy strategy = factory.getStrategy(request.getPaymentType());
        // 执行支付模板流程
        return strategy.pay(request);
    }
}
思考2

问题:

还是非常庞大的判断语句,量很大,这时候如果使用状态模式,会不会造成类爆炸呢?因为处理的情况很多,就像要考虑非常多的情况,而且在组装上下文的时候会定义很多变量,造成代码臃肿。

方案:

当状态数量过多时,纯粹的状态模式确实会变得笨重。此时,我们需要对其进行简化,牺牲一部分“纯粹性”来换取可维护性。

示例:

public enum OrderStatus {
    // 枚举实例:状态名(描述), 并实现该状态的行为
    UNPAID("待支付") {
        @Override
        public void next(Order order) {
            order.setStatus(PAID);
        }
        @Override
        public void cancel(Order order) {
            order.setStatus(CANCELLED);
            refundService.process(order); // 引入外部服务
        }
    },
    PAID("已支付") {
        @Override
        public void next(Order order) {
            if (order.getItems().stream().anyMatch(item -> item.isInternational())) {
                order.setStatus(CUSTOMS_CHECK); // 国际商品进入报关状态
            } else {
                order.setStatus(SHIPPED);
            }
        }
        @Override
        public void cancel(Order order) {
            // ...复杂的退款逻辑
            order.setStatus(REFUNDING);
        }
    },
    CUSTOMS_CHECK("报关中"),
    SHIPPED("已发货"),
    // ... 其他很多状态
    ;

    private String description;

    OrderStatus(String description) {
        this.description = description;
    }

    // 定义状态的行为方法
    public void next(Order order) {
        throw new UnsupportedOperationException("状态[" + this + "]不支持该操作");
    }
    public void cancel(Order order) {
        throw new UnsupportedOperationException("状态[" + this + "]不支持该操作");
    }

    // 可以持有外部服务的引用(通过setter注入)
    private static RefundService refundService;
    public static void setRefundService(RefundService service) {
        OrderStatus.refundService = service;
    }
}

// 上下文类(变得非常轻量)
public class Order {
    private OrderStatus status;
    // ... other fields

    public void nextStatus() {
        status.next(this); // 将行为委托给枚举状态
    }
    public void cancel() {
        status.cancel(this);
    }
    // Setter for status...
}

七、怎么正确使用状态模式

💡 既然你能读到这儿就说明了一些问题,刚开始不用太纠结状态模式,之后一定要怎么使用这个模式,目的就是让你看到这些东西,以至于在后面你遇到问题的时候能瞬间反应过来,这需要使用状态模式,到时候在搜索怎么解决即可,不要等到那个时候你不知道怎么解决,就算有ai又如何,你不知道怎么发布命令是最可悲的,机会一定是留给有准备的人

6. 观察者模式(重点!)

注意:观察者在开发中使用频率很高,尤其是各种框架的实现都会用到,是我们必须要掌握的一种模式

一、核心思想

📌 观察者模式代表了典型的一对多关系只要一个发生变化,其余的都要发生变化,但是这个变化并不是直接调用它们的业务代码,而是使用通知(接口)的形式让他们自己去调用自己的业务代码。典型的例子就是报社,文章发布后通过订阅者列表不间断发送通知。

报纸订阅

  1. 主题 (Subject):报社。它维护着一份订阅者列表。

  2. 观察者 (Observer):订阅报纸的读者。

  3. 订阅 (Subscribe):读者向报社提供自己的联系方式,表示“有新闻就通知我”。这个过程叫注册 (Register) 或订阅。

  4. 通知 (Notify):每当报社出版了新报纸(状态改变),它就会按照订阅者列表,将报纸邮寄给每一位读者。报社不关心读者是直接阅读、垫桌脚还是用来生火,它只负责“发出通知”。

  5. 退订 (Unsubscribe):读者不想再看了,可以通知报社将其从列表中移除。

二、 要解决的核心问题

反面教材:紧耦合的天气数据站

假设有一个气象站WeatherData,当它的测量数据(温度、湿度、气压)更新时,需要更新三个不同的布告板:当前条件、气象统计和天气预报。

public class WeatherData {
    private float temperature;
    private float humidity;
    private float pressure;

    // 对具体布告板的硬编码引用(紧耦合!)
    private CurrentConditionsDisplay currentDisplay;
    private StatisticsDisplay statisticsDisplay;
    private ForecastDisplay forecastDisplay;

    // ... 其他可能有的布告板

    // 当测量更新时,需要手动调用每个布告板的更新方法
    public void measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        // 这里紧密依赖于具体的实现类
        currentDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

        // 如果要新增一个布告板,必须修改这里的代码!
        // newDisplay.update(temp, humidity, pressure);
    }

    // ... getters and setters
}

这种代码的弊端

  • 违反开闭原则:每次添加或删除一个布告板,都必须修改WeatherData类的measurementsChanged()方法。

  • 紧耦合:WeatherData类直接依赖于所有具体的布告板类,而不是依赖于抽象。

  • 难以复用:WeatherData类很难被复用到其他需要类似通知机制的项目中,因为它扛着一堆具体的依赖

三、主要角色

1. 主题接口(Subject Interface)
  • 定义:声明了三个核心方法:registerObserver(), removeObserver(), notifyObservers()。

  • 职责:为所有具体主题提供一个统一的接口,以便它们可以管理观察者。

  • 注意:主题接口一般只有一个,但是具体主题可以有多个,也就是多个不同的观察者模块。

2. 具体主题 (Concrete Subject)
  • 定义:实现了Subject接口。

  • 职责

    • 维护一个观察者对象的集合(如List)。

    • 在自身状态改变时,调用notifyObservers()方法,遍历观察者集合并调用每个观察者的更新方法。

    • 提供方法获取自身的状态(getState())。

3. 观察者接口 (Observer Interface)
  • 定义:通常只声明一个方法,如update()。当主题状态改变时,这个方法会被调用。

  • 职责:为所有具体观察者提供一个统一的更新接口。

4. 具体观察者 (Concrete Observer)
  • 定义:实现了Observer接口。

  • 职责

    • 在update()方法中,编写收到通知后需要执行的逻辑。通常在此方法中会从主题对象那里拉取 (Pull) 所需的数据。

    • 维护一个对主题对象的引用(通常通过在构造函数中注册自己来实现),以便可以随时订阅或退订。

四、代码实现示例(Java - 气象站案例)

第1步:定义主题接口和观察者接口

// 主题接口
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

// 观察者接口
public interface Observer {
    void update(); // 当主题状态改变时,此方法被调用
}

第2步:实现具体主题 (WeatherData)

// 具体主题
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
    private List<Observer> observers; // 维护一个观察者列表
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    // 实现主题接口
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        // 重要:通知所有观察者
        for (Observer observer : observers) {
            observer.update(); // 只是通知“我变了”,观察者自己来拉取数据
        }
    }

    // 当从气象站得到新测量值时,此方法被调用
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged(); // 通知观察者们
    }

    // 内部方法,现在变得非常简单
    private void measurementsChanged() {
        notifyObservers(); // 核心:只需调用这一个方法
    }

    // 提供getter方法,供观察者“拉取”数据
    public float getTemperature() { return temperature; }
    public float getHumidity() { return humidity; }
    public float getPressure() { return pressure; }
}

第3步:实现具体观察者 (布告板)

// 具体观察者:当前条件布告板
public class CurrentConditionsDisplay implements Observer {
    private float temperature;
    private float humidity;
    private Subject weatherData; // 持有主题的引用,用于退订和拉取数据

    // 构造函数中完成注册
    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this); // 将自己注册为观察者
    }

    @Override
    public void update() {
        // 从主题那里“拉取”我们需要的数据
        this.temperature = ((WeatherData) weatherData).getTemperature();
        this.humidity = ((WeatherData) weatherData).getHumidity();
        display();
    }

    public void display() {
        System.out.println("Current conditions: " + temperature + "°C and " + humidity + "% humidity");
    }
}

// 具体观察者:天气预报布告板
public class ForecastDisplay implements Observer {
    private float lastPressure;
    private float currentPressure;
    private Subject weatherData;

    public ForecastDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
        this.currentPressure = 29.92f; // 初始值
    }

    @Override
    public void update() {
        lastPressure = currentPressure;
        currentPressure = ((WeatherData) weatherData).getPressure();
        display();
    }

    public void display() {
        // 简单的天气预报逻辑
        if (currentPressure > lastPressure) {
            System.out.println("Forecast: Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("Forecast: More of the same");
        } else {
            System.out.println("Forecast: Watch out for cooler, rainy weather");
        }
    }
}

第4步:测试客户端

public class WeatherStation {
    public static void main(String[] args) {
        // 1. 创建主题(气象站)
        WeatherData weatherData = new WeatherData();

        // 2. 创建观察者(布告板),并在创建时完成注册
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

        // 3. 模拟新的气象测量数据到来
        System.out.println("--- First Measurement ---");
        weatherData.setMeasurements(25, 65, 1013);

        System.out.println("\n--- Second Measurement ---");
        weatherData.setMeasurements(23, 70, 1012);

        // 4. 动态移除一个观察者
        System.out.println("\n--- Removing Forecast Display ---");
        weatherData.removeObserver(forecastDisplay);

        // 5. 再次更新数据,只有CurrentConditionsDisplay会收到通知
        System.out.println("\n--- Third Measurement (after removal) ---");
        weatherData.setMeasurements(20, 90, 1015);
    }
}

五、 推模型 vs. 拉模型

上面的例子是典型的拉模型 (Pull Model):

Subject.notifyObservers() ·只负责通知“我变了”·。

Observer.update() 方法不接收参数,观察者需要主动调用主题的getter方法去拉取它需要的数据。

  • 优点:主题不知道观察者需要什么数据,通用性强。

  • 缺点:观察者必须知道主题的接口,以调用正确的getter方法。

另一种实现是推模型 (Push Model):

Subject.notifyObservers() 方法会将数据作为参数传递给Observer.update()方法

Observer.update(float temp, float humidity, float pressure) 直接接收数据。

  • 优点:对于观察者来说更方便。

  • 缺点:不够灵活。如果主题推送了大量数据,但某个观察者只关心其中一小部分,就会造成浪费。并且,改变推送的数据结构会影响到所有观察者。

Java内置的Observable类使用的是拉模型。 在实际应用中,拉模型更为常见和推荐,因为它松耦合的程度更高。

六、 模式的优势与适用场景

优势:
  • 松耦合:主题和观察者之间抽象耦合。主题只知道观察者实现了某个接口,而不知道其具体类。

  • 开闭原则:可以轻松地增加新的观察者,而无需修改主题的代码。

  • 可以在运行时动态地建立对象间的关系:观察者可以随时订阅或取消订阅,非常灵活。

适用场景:
  • 当一个对象的改变需要同时改变其他对象,但不知道具体有多少对象需要改变时。

  • 当一个对象必须通知其他对象,但又希望这些对象是松散耦合的。

经典应用:
  • GUI事件处理:几乎所有UI框架(Java Swing, Android, JavaFX)的按钮点击、鼠标移动等事件监听机制都是观察者模式。

  • 发布-订阅 (Pub/Sub) 系统:这是观察者模式的分布式升级版,如Kafka, Redis Pub/Sub。

  • MVC架构:模型(Model)作为主题,视图(View)作为观察者。当模型数据发生变化时,会自动通知所有视图更新。

7 中介者模式

注意:这里的中介者不是我们生活中所指的那种中介,更形象的应该是媒介的意思

一、核心思想

📌 拿机场的塔台举例,因为跑道数量少,飞机又很多,国内国外等不同地区的飞机之间有很大的关系,如果让他们自己沟通,比如我先降落你后降落这种,这将是一个复杂的网状关系,不同飞机之间的耦合度太高了,就是所谓的牵一发而动全身,为了解决这种问题,于是出现了塔台来控制所有的飞机,塔台就是一个中介者。

二、具体对象

1. 中介者 (Mediator)
  • 定义:一个接口,定义了用于与各同事对象进行通信的方法。它通常是一个接口或抽象类。

  • 职责:负责协调各个同事对象之间的交互。它知道所有同事对象及其用途。

2. 具体中介者 (Concrete Mediator)
  • 定义:实现了Mediator接口。

  • 职责

    • 维护对所有具体同事对象的引用。

    • 实现中介者接口中声明的通信方法,包含对象之间交互的具体逻辑。

    • 在收到一个同事对象的通知后,它可以根据业务规则与其他同事对象进行通信。

3. 同事类 (Colleague)
  • 定义:所有需要与其他对象通信的对象都属于同事类。它们都知道中介者对象。

  • 职责

    • 每个同事对象在需要与其他对象通信时,都与它的中介者进行通信。

    • 通常会在构造函数中接收一个中介者对象,或者通过setter方法注入。

三、错误示例(订单系统)

想象一个电商系统的订单履约流程。一个用户下单后,需要多个后端服务协同工作:

  • 库存服务 (Inventory Service):检查并锁定库存。

  • 支付服务 (Payment Service):处理扣款。

  • 物流服务 (Shipping Service):创建运单,安排发货。

  • 通知服务 (Notification Service):给用户发送短信和邮件通知。

// 反面教材:OrderService 承担了所有协调职责,耦合严重
@Service
public class OrderService {

    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private ShippingService shippingService;
    @Autowired
    private NotificationService notificationService;

    public void fulfillOrder(Order order) {
        try {
            // 1. 调用库存服务
            boolean lockSuccess = inventoryService.lockInventory(order);
            if (!lockSuccess) {
                throw new RuntimeException("Inventory lock failed");
            }

            // 2. 调用支付服务
            boolean paySuccess = paymentService.processPayment(order);
            if (!paySuccess) {
                // 支付失败,需要释放之前锁定的库存!
                inventoryService.unlockInventory(order);
                throw new RuntimeException("Payment failed");
            }

            // 3. 调用物流服务
            String trackingNumber = shippingService.shipOrder(order);
            order.setTrackingNumber(trackingNumber);

            // 4. 调用通知服务
            notificationService.sendOrderShippedNotification(order);

            order.setStatus(OrderStatus.FULFILLED);
            // ... 更新订单状态

        } catch (Exception e) {
            // 异常处理极其复杂:需要根据失败发生在哪一步,决定回滚哪些操作
            handleRollback(order, e);
        }
    }

    private void handleRollback(Order order, Exception e) {
        // 混乱的回滚逻辑,需要判断哪些步骤已经执行,哪些需要补偿
        // 例如:如果支付成功了但发货失败,可能需要触发退款
        // 这段代码会变得非常臃肿和难以维护
    }
}

注意:上面的例子是我们在开发中经常使用的一种操作,往往一个业务模块会和其它业务模块大量集成,虽然改改也能用,单从设计来说不合适,因为这是独立的业务模块,就不说你一旦出问题或者要修改麻烦,主要是这种紧耦合的设计模式太容易出现错误,非常难扩展。

四、正确示例

第1步:定义中介者接口

// 中介者接口:订单协调器
public interface OrderFulfillmentMediator {
    /**
     * 协调处理一个订单的完整履约流程
     * @param order 需要处理的订单
     */
    void coordinateFulfillment(Order order);
}

第2步:定义同事类接口(所有服务需要遵守的契约)

// 同事类接口:订单流程处理组件
public interface OrderProcessComponent {
    /**
     * 处理订单
     * @param order 订单
     * @return 处理结果
     */
    ProcessResult process(Order order);

    /**
     * 如果整个流程失败,执行补偿操作(回滚)
     * @param order 需要补偿的订单
     */
    void compensate(Order order);
}

// 处理结果的通用对象
@Data
public class ProcessResult {
    private boolean success;
    private String message;
    // ... 其他字段
}

第3步:实现具体同事(各个微服务)

// 具体同事:库存服务
@Service
public class InventoryService implements OrderProcessComponent {
    @Override
    public ProcessResult process(Order order) {
        log.info("锁定订单 {} 的库存", order.getId());
        // 实际业务逻辑:检查并锁定库存
        boolean lockSuccess = // ... 调用库存DAO或Feign客户端
        return new ProcessResult(lockSuccess, lockSuccess ? "库存锁定成功" : "库存不足");
    }

    @Override
    public void compensate(Order order) {
        log.info("释放订单 {} 的库存", order.getId());
        // 补偿逻辑:释放锁定的库存
        // inventoryDao.unlock(order);
    }
}

// 具体同事:支付服务
@Service
public class PaymentService implements OrderProcessComponent {
    @Override
    public ProcessResult process(Order order) {
        log.info("处理订单 {} 的支付", order.getId());
        // 实际业务逻辑:调用支付网关
        boolean paySuccess = // ... 调用支付API
        return new ProcessResult(paySuccess, paySuccess ? "支付成功" : "支付失败");
    }

    @Override
    public void compensate(Order order) {
        log.info("为订单 {} 发起退款", order.getId());
        // 补偿逻辑:调用退款API
        // refundService.refund(order);
    }
}
// ... 类似地实现 ShippingService, NotificationService

第4步:实现具体中介者(协调器)

@Service
public class OrderFulfillmentCoordinator implements OrderFulfillmentMediator {

    // 中介者知道所有需要协调的组件(同事)
    private final List<OrderProcessComponent> componentsInOrder;

    // 通过构造器注入,顺序即为流程顺序
    public OrderFulfillmentCoordinator(InventoryService inventoryService,
                                      PaymentService paymentService,
                                      ShippingService shippingService,
                                      NotificationService notificationService) {
        this.componentsInOrder = List.of(
            inventoryService,   // 步骤1: 锁库存
            paymentService,     // 步骤2: 支付
            shippingService,    // 步骤3: 发货
            notificationService // 步骤4: 通知
        );
    }

    @Override
    @Transactional // 可能需要在协调器层面管理事务(或使用Saga)
    public void coordinateFulfillment(Order order) {
        List<OrderProcessComponent> executedComponents = new ArrayList<>();
        // 记录哪些步骤已成功执行,用于失败时补偿

        try {
            // 按顺序执行每一个步骤
            for (OrderProcessComponent component : componentsInOrder) {
                ProcessResult result = component.process(order);
                if (!result.isSuccess()) {
                    // 如果某一步失败,则补偿前面所有已成功的步骤
                    compensateExecutedComponents(order, executedComponents);
                    throw new OrderFulfillmentException("Order fulfillment failed at step: " + component.getClass().getSimpleName() + ", Reason: " + result.getMessage());
                }
                // 执行成功,记录下来
                executedComponents.add(component);
            }
            // 所有步骤成功完成
            order.setStatus(OrderStatus.FULFILLED);
            log.info("订单 {} 履约成功", order.getId());

        } catch (Exception e) {
            log.error("订单 {} 履约流程异常", order.getId(), e);
            compensateExecutedComponents(order, executedComponents);
            order.setStatus(OrderStatus.FAILED);
            throw new OrderFulfillmentException("Order fulfillment failed", e);
        }
    }

    /**
     * 补偿已执行的步骤(Saga模式的实现)
     * @param order 订单
     * @param executedComponents 已成功执行的组件列表(按执行顺序)
     */
    private void compensateExecutedComponents(Order order, List<OrderProcessComponent> executedComponents) {
        // 注意:补偿顺序需要与执行顺序相反(后执行的先回滚)
        Collections.reverse(executedComponents);
        for (OrderProcessComponent component : executedComponents) {
            try {
                component.compensate(order);
            } catch (Exception ex) {
                // 补偿操作本身也可能失败,需要记录日志和告警,但通常不阻断其他补偿
                log.error("补偿操作执行失败 for component: " + component.getClass().getSimpleName(), ex);
            }
        }
    }
}

第5步:客户端(OrderService)使用

@Service
public class OrderService {
    // 现在只依赖于中介者,而不是所有服务
    @Autowired
    private OrderFulfillmentMediator fulfillmentMediator;

    public void placeOrder(Order order) {
        // 1. 保存订单等前置操作...
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);

        // 2. 将复杂的履约流程委托给中介者协调器!
        try {
            fulfillmentMediator.coordinateFulfillment(order);
        } catch (OrderFulfillmentException e) {
            // 处理异常
        }
        // 3. 更新订单状态等后置操作...
        orderRepository.save(order);
    }
}

五、 新模式下的优势

  • OrderService(客户端)解耦:它不再关心履约的具体步骤和复杂的补偿逻辑,只负责发起流程。它现在只依赖一个OrderFulfillmentMediator接口。

  • 流程集中化:所有业务流程和Saga分布式事务补偿逻辑都集中在了OrderFulfillmentCoordinator中,变得清晰、可维护、可测试。

  • 开闭原则:要增加一个新的步骤(例如,在支付前调用一个RiskControlService),你只需要:

    • 让新服务实现OrderProcessComponent接口。

    • 修改协调器OrderFulfillmentCoordinator的构造器,将新服务插入到流程列表的正确位置。

    • 完全不需要修改OrderService、InventoryService等其他任何现有服务。

  • 单一职责

    • OrderService:负责订单的生命周期管理。

    • InventoryService等:只负责自己领域的业务操作和补偿。

    • OrderFulfillmentCoordinator:只负责流程编排和协调。

8、迭代器模式(Iterator)

这里就不详细说明迭代器模式了,在正常开发中我们经常使用Iterator进行遍历,可能最大的作用就是为了动态判断移除某一些元素,或者说在并行流程中使用迭代器,操作同一个集合。

根据前面各种模式的介绍我们也能大致明白,核心就是包装了一层,将关系维护到Iterator中,由Iterator动态的和集合进行通信,我们不会直接操作集合,这样就能实现更多的功能,也不会出现互相影响的现象,有兴趣的可以研究一下源码,还是比较容易理解的。

9、访问者模式(很复杂、很难懂)

注意:访问者模式确实相当复杂,它的出现都是为了系统的底层或者说一个第三方模块的底层,可以理解为一个工具的开发者。鄙人看完之后还是不太懂,所以在这里简单说一下场景还有解决的代码示例,大家如果能看懂例子那就去搜一下吧。

错误代码示例

// 元素基类
public abstract class JsonNode {
    // ... 公共属性和方法
}

// 具体元素:字符串节点
public class StringNode extends JsonNode {
    private String value;
    public String getValue() { return value; }

    // 如果要添加格式化的操作,就得在这里加方法
    public String toFormattedString() {
        return "\"" + value + "\"";
    }
    // 如果再要添加验证的操作,又得加方法
    public boolean isValid() {
        return value != null;
    }
    // 每增加一个新操作,都要修改所有元素类!
}

// 具体元素:数字节点
public class NumberNode extends JsonNode {
    private Number value;
    public Number getValue() { return value; }

    // 同样,必须重复实现所有操作
    public String toFormattedString() {
        return value.toString();
    }
    public boolean isValid() {
        return value != null;
    }
}

// 具体元素:对象节点
public class ObjectNode extends JsonNode {
    private Map<String, JsonNode> properties = new HashMap<>();
    public Map<String, JsonNode> getProperties() { return properties; }

    // 实现变得非常复杂!
    public String toFormattedString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{\n");
        for (Map.Entry<String, JsonNode> entry : properties.entrySet()) {
            sb.append("  \"").append(entry.getKey()).append("\": ")
              .append(entry.getValue().toFormattedString()).append(",\n");
        }
        sb.append("}");
        return sb.toString();
    }
    public boolean isValid() {
        for (JsonNode node : properties.values()) {
            if (!node.isValid()) return false;
        }
        return true;
    }
}

正确示例

第1步:定义元素接口和访问者接口

// 元素接口
public interface JsonNode {
    void accept(JsonVisitor visitor);
}

// 访问者接口
public interface JsonVisitor {
    // 为每一种具体的元素类型定义一个访问方法
    void visit(StringNode stringNode);
    void visit(NumberNode numberNode);
    void visit(ObjectNode objectNode);
    // 添加新的元素类型需要修改此接口,这是模式的缺点
}

第2步:实现具体元素

// 具体元素:字符串节点
public class StringNode implements JsonNode {
    private String value;
    public StringNode(String value) { this.value = value; }
    public String getValue() { return value; }

    @Override
    public void accept(JsonVisitor visitor) {
        visitor.visit(this); // 关键:调用访问者专门处理StringNode的方法
    }
}

// 具体元素:数字节点
public class NumberNode implements JsonNode {
    private Number value;
    public NumberNode(Number value) { this.value = value; }
    public Number getValue() { return value; }

    @Override
    public void accept(JsonVisitor visitor) {
        visitor.visit(this); // 关键:调用访问者专门处理NumberNode的方法
    }
}

// 具体元素:对象节点
public class ObjectNode implements JsonNode {
    private Map<String, JsonNode> properties = new HashMap<>();
    public Map<String, JsonNode> getProperties() { return properties; }
    public void addProperty(String key, JsonNode value) {
        properties.put(key, value);
    }

    @Override
    public void accept(JsonVisitor visitor) {
        visitor.visit(this); // 关键:调用访问者专门处理ObjectNode的方法

        // 通常,容器元素还需要让访问者访问其子元素
        for (JsonNode node : properties.values()) {
            node.accept(visitor);
        }
    }
}

第3步:实现具体访问者(操作实现)

// 具体访问者:格式化访问者
public class FormatVisitor implements JsonVisitor {
    private StringBuilder result = new StringBuilder();

    public String getResult() {
        return result.toString();
    }

    @Override
    public void visit(StringNode stringNode) {
        // 集中实现所有字符串节点的格式化逻辑
        result.append("\"").append(stringNode.getValue()).append("\"");
    }

    @Override
    public void visit(NumberNode numberNode) {
        // 集中实现所有数字节点的格式化逻辑
        result.append(numberNode.getValue());
    }

    @Override
    public void visit(ObjectNode objectNode) {
        // 集中实现所有对象节点的格式化逻辑
        result.append("{");
        boolean first = true;
        for (Map.Entry<String, JsonNode> entry : objectNode.getProperties().entrySet()) {
            if (!first) {
                result.append(", ");
            }
            result.append("\"").append(entry.getKey()).append("\": ");
            entry.getValue().accept(this); // 递归访问子节点
            first = false;
        }
        result.append("}");
    }
}

// 具体访问者:验证访问者
public class ValidationVisitor implements JsonVisitor {
    private boolean isValid = true;

    public boolean isValid() {
        return isValid;
    }

    @Override
    public void visit(StringNode stringNode) {
        if (stringNode.getValue() == null) {
            isValid = false;
            System.err.println("String node cannot be null");
        }
    }

    @Override
    public void visit(NumberNode numberNode) {
        if (numberNode.getValue() == null) {
            isValid = false;
            System.err.println("Number node cannot be null");
        }
    }

    @Override
    public void visit(ObjectNode objectNode) {
        for (JsonNode node : objectNode.getProperties().values()) {
            node.accept(this); // 递归验证子节点
            if (!isValid) break; // 提前终止
        }
    }
}

第4步:客户端使用

public class Client {
    public static void main(String[] args) {
        // 1. 构建一个复杂的JSON对象结构
        ObjectNode root = new ObjectNode();
        root.addProperty("name", new StringNode("Alice"));
        root.addProperty("age", new NumberNode(30));
        
        ObjectNode address = new ObjectNode();
        address.addProperty("city", new StringNode("New York"));
        address.addProperty("zip", new NumberNode(10001));
        root.addProperty("address", address);

        // 2. 使用格式化访问者来操作这个结构
        FormatVisitor formatter = new FormatVisitor();
        root.accept(formatter); // 开始遍历和格式化
        System.out.println("Formatted JSON:");
        System.out.println(formatter.getResult());

        System.out.println("\n------------------------\n");

        // 3. 使用验证访问者来操作同一个结构
        ValidationVisitor validator = new ValidationVisitor();
        root.accept(validator); // 开始遍历和验证
        System.out.println("Is JSON valid? " + validator.isValid());

        // 4. 轻松添加新操作:计算JSON大小
        //    只需要新建一个 SizeCalculationVisitor,无需修改任何JsonNode类!
        SizeCalculationVisitor sizeCalculator = new SizeCalculationVisitor();
        root.accept(sizeCalculator);
        System.out.println("JSON size in bytes: " + sizeCalculator.getTotalSize());
    }
}

10. 备忘录模式(简单)

// 备忘录(就是一个存数据的盒子)
public class GameMemento {
    private final String state; // 状态:如关卡、血量、装备等
    public GameMemento(String stateToSave) { this.state = stateToSave; }
    public String getSavedState() { return state; } // 只有Originator应该调用这个方法
}

// 发起人(游戏)
public class Game {
    private String currentState; // 当前游戏状态

    public void play() { currentState = "Playing Level 5, HP: 100"; }
    public void die() { currentState = "Game Over"; }

    // 保存:将当前状态打包成一个备忘录对象
    public GameMemento save() {
        return new GameMemento(currentState);
    }

    // 恢复:从备忘录对象中读取之前的状态
    public void load(GameMemento memento) {
        currentState = memento.getSavedState();
    }

    public void printState() { System.out.println(currentState); }
}

// 管理者(存档管理器)
public class SaveManager {
    private GameMemento savepoint; // 只负责保管,不负责查看和修改

    public void saveGame(Game game) {
        savepoint = game.save();
    }
    public void loadGame(Game game) {
        if (savepoint != null) {
            game.load(savepoint);
        }
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        Game game = new Game();
        SaveManager saveManager = new SaveManager();

        game.play(); // 玩游戏
        game.printState(); // Output: Playing Level 5, HP: 100

        saveManager.saveGame(game); // 存档!

        game.die(); // 玩死了
        game.printState(); // Output: Game Over

        saveManager.loadGame(game); // 读档!
        game.printState(); // Output: Playing Level 5, HP: 100 (恢复到存档点)
    }
}

这个模式实际上就是一个简单的存储,打一个标记的作用,代码实现也很简单,就不做过多的说明。

11. 解释器模式(最复杂,普通业务开发99.99%用不到,有兴趣的可以自己搜一下)

总结

任重而道远啊兄弟们,多的不说,大家加油吧!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐