本笔记涵盖 23 种经典设计模式,按创建型、结构型、行为型三大类整理。每个模式配有生活类比、核心思想、结构图解、实现要点、真实应用案例、适用场景、优缺点分析及常见面试题。
目录
- 设计原则(SOLID)
- 创建型模式(5种)
- 结构型模式(7种)
- 行为型模式(11种)
- 模式之间的关系
- 模式速查表
一、设计原则(SOLID)
S — 单一职责原则(Single Responsibility Principle)
一个类只应该承担一种职责,只做一件事。如果一个类承担了多种职责,那么它发生变化的原因就会有多个,违反了高内聚低耦合的原则。
应用案例:在电商系统中,OrderService 只负责订单的创建、查询、取消等订单相关操作,不应该同时负责发送邮件通知或库存管理。邮件通知交给 NotificationService,库存管理交给 InventoryService。
O — 开闭原则(Open/Closed Principle)
软件实体对扩展开放、对修改关闭。当需要增加新功能时,应该通过增加新代码来实现,而不是修改已有的代码。这是设计模式中最核心的原则。
应用案例:支付系统中,新增"微信支付"时只需要新增 WechatPayStrategy 类,不需要修改已有的 PayService 代码。通过策略模式实现开闭原则。
L — 里氏替换原则(Liskov Substitution Principle)
所有引用父类的地方必须能够透明地替换为子类的对象,且程序行为不变。子类不能改变父类已有方法的语义。
应用案例:Bird 类有 fly() 方法,但 Penguin(企鹅)继承 Bird 后不能飞,重写 fly() 抛出异常——这违反了里氏替换原则。正确做法是设计 FlyingBird 和 Penguin 两个独立的类。
I — 接口隔离原则(Interface Segregation Principle)
客户端不应该被迫依赖它不使用的接口。接口应该小而专,而不是大而全。一个类对另一个类的依赖应该建立在最小的接口上。
应用案例:不应该设计一个庞大的 IEmployee 接口包含 work()、eat()、sleep() 方法,因为机器人员工不需要 eat() 和 sleep()。应该拆分为 IWorkable、IEatable、ISleepable 三个小接口。
D — 依赖倒置原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,二者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。简单说就是"面向接口编程,而不是面向实现编程"。
应用案例:OrderService 依赖 IRepository 接口而不是具体的 MySQLRepository。当需要切换到 MongoDBRepository 时,只需要更改注入的实现类,OrderService 完全不用改。
二、创建型模式(5种)— 关注"怎么创建对象"
创建型模式将对象的创建和使用分离,降低了代码的耦合度。
01 单例模式 Singleton
一句话理解
整个程序运行期间,某个类只能有一个实例对象。
生活类比
全国只能有一个皇帝。不管谁去请旨,面对的都是同一个人。如果有人想再立一个皇帝,那是谋反。
核心思想
构造方法私有化 + 静态变量 + 全局访问点 = 单例。
| private 构造方法 | 外部不能 new |
| static 实例变量 | 全局唯一 |
| getInstance() | 唯一获取入口 |
实现方式详解
方式一:双重检查锁(最常用)
- 第一次检查:如果已经创建了,直接返回,不需要进锁(性能优化)
- 第二次检查:防止多个线程同时通过了第一次检查后重复创建
- 必须加 volatile 关键字:instance = new Singleton() 不是原子操作,JVM 可能重排序为先赋值引用再初始化对象,volatile 可防止此问题
方式二:枚举方式(最简洁,推荐)
- 天然防止反射攻击(反射无法创建枚举实例)
- 天然线程安全(JVM 保证)
- 代码最简洁,是 Effective Java 推荐的方式
真实应用案例
| Runtime 类 | Java 的 Runtime.getRuntime() 就是单例,整个 JVM 只有一个运行时实例 |
| Spring Bean | Spring 容器中默认 scope="singleton",一个 Bean 只有一个实例 |
| 数据库连接池 | HikariCP、Druid 等连接池都是单例,全局共享一个池 |
| 日志框架 | Log4j、Logback 的 Logger 实例通常是单例 |
| 配置中心 | Apollo、Nacos 的配置客户端是单例,全局读取同一份配置 |
| 线程池 | Executors.newFixedThreadPool() 返回的线程池是单例 |
| Windows 回收站 | 系统中只有一个回收站实例 |
| 网站访问计数器 | 全局唯一的计数器实例 |
适用场景
| 数据库连接池 | 全局共享一个连接池,避免重复创建 |
| 线程池 | 统一管理线程资源 |
| 配置管理器 | 全局读取同一份配置 |
| 日志对象 | 统一日志输出,保证线程安全 |
| 缓存管理器 | 全局共享缓存实例 |
优缺点
| 节省内存(只创建一次) | 不利于单元测试(全局状态难以 mock) |
| 避免重复创建对象 | 反射可以破坏(枚举方式除外) |
| 数据共享方便 | 违反单一职责(既管业务又管生命周期) |
| 懒加载节省资源 | 在分布式系统中需要特殊处理 |
常见面试题
- 双重检查锁为什么要加 volatile? instance = new Singleton() 不是原子操作,JVM 可能重排序。加 volatile 可防止此问题。
- 反射能破坏单例吗? 可以,通过 Constructor.setAccessible(true) 可以调用私有构造方法。但枚举方式天然防御反射。
- 单例在分布式系统中有什么问题? 单例是 JVM 级别的,不同 JVM 有不同实例。分布式系统需要使用分布式锁或 Redis 等方案保证全局唯一。
02 工厂模式 Factory
一句话理解
不直接 new 对象,而是通过工厂方法来创建对象。
生活类比
你去餐厅不会自己进厨房做菜,而是告诉服务员"来一份宫保鸡丁"。厨师(工厂)根据你的需求做出对应的菜(对象)。
三种工厂模式对比
简单工厂 → 工厂方法 → 抽象工厂
(一个工厂 (每个产品 (每个产品族
创所有产品) 有自己的工厂) 有自己的工厂)
一、简单工厂 (Simple Factory)
- 一个工厂类,根据参数(通常是 switch-case)创建不同的产品
- 优点:逻辑集中,使用简单
- 缺点:每新增一种产品就要修改工厂代码,违反开闭原则
- 也被称为"静态工厂方法"
二、工厂方法 (Factory Method)
- 每种产品有自己的工厂,新增产品只需新增工厂类
- 优点:符合开闭原则,新增产品不改已有代码
- 缺点:类的数量增多
三、抽象工厂 (Abstract Factory)
- 一个工厂生产一整套相关产品(产品族)
- 例如:精灵族的城堡+国王+军队,兽人族的城堡+国王+军队
- 优点:保证产品族风格统一
- 缺点:新增产品类型需要修改所有工厂
真实应用案例
| JDK 集合框架 | 工厂方法 | List.of()、Set.of()、Map.of() 根据参数返回不同的集合实现 |
| 日志框架 | 简单工厂 | LoggerFactory.getLogger() 根据类名返回不同的 Logger 实例 |
| Spring BeanFactory | 抽象工厂 | 根据配置创建不同类型的 Bean |
| 数据库驱动 | 工厂方法 | DriverManager.getConnection() 根据 URL 创建不同的数据库连接 |
| 跨平台 UI | 抽象工厂 | Windows/Mac/Linux 各有 Button、TextBox、Checkbox 的工厂 |
| 消息发送 | 工厂方法 | 根据类型创建 SMS、Email、Push 的发送器 |
| 支付系统 | 简单工厂 | 根据支付方式返回微信、支付宝、银行卡的支付处理器 |
| JUnit | 工厂方法 | @Parameterized 测试通过工厂方法生成测试数据 |
| 棋盘游戏 | 抽象工厂 | 不同棋类(象棋、围棋、五子棋)各有棋盘、棋子、规则的工厂 |
三种工厂怎么选?
| 简单工厂 | 产品种类少,创建逻辑简单 | 根据类型返回不同硬币 |
| 工厂方法 | 产品种类多,会频繁扩展 | 不同品牌各自创建自己的手机 |
| 抽象工厂 | 需要一整套风格统一的产品 | 游戏中不同种族的城堡+国王+军队 |
优缺点
| 解耦:调用方不需要知道具体类名 | 增加新产品时需要增加新的工厂类 |
| 符合开闭原则(工厂方法/抽象工厂) | 类的数量会增多 |
| 统一管理对象创建 | 简单工厂违反开闭原则 |
| 代码可维护性高 | 抽象工厂新增产品类型困难 |
03 建造者模式 Builder
一句话理解
把复杂对象的"组装过程"和"最终产品"分离开。
生活类比
你去肯德基点套餐:先选汉堡,再选饮料,最后选薯条。服务员按你的要求一步步组装,最后给你一个完整的套餐。你不需要知道汉堡怎么做、饮料怎么调——你只管选,服务员(Builder)负责组装。
核心角色
| Director(指挥者) | 调用 Builder 的步骤,定义组装流程 |
| Builder(建造者) | 负责一步步组装产品 |
| Product(产品) | 最终得到的对象 |
核心特点
- 链式调用:每个构建方法返回 this,支持 builder.buildType().buildRegion().build() 这样的流畅写法
- 分步构建:将复杂对象的创建分解为多个简单的步骤
- 产品复制:build() 方法中复制结果返回新对象,防止外部修改 Builder 内部状态
真实应用案例
| Lombok @Builder | 自动生成 Builder 代码,简化 POJO 对象创建 |
| Java StringBuilder | new StringBuilder().append("a").append("b").toString() 就是链式构建 |
| Guava ImmutableList | ImmutableList.builder().add(1).add(2).build() 构建不可变集合 |
| OkHttp Request | new Request.Builder().url(…).header(…).build() 构建 HTTP 请求 |
| Retrofit | new Retrofit.Builder().baseUrl(…).addConverterFactory(…).build() |
| AlertDialog.Builder | Android 中构建对话框 |
| Notification.Builder | Android 中构建通知 |
| Document Object Model | XML/HTML 文档的构建过程 |
| Protobuf | Protocol Buffers 的消息构建器 |
| MyBatis SqlBuilder | 动态 SQL 构建 |
Builder vs 工厂模式
| 关注点 | 对象怎么组装 | 对象创建哪个 |
| 产品 | 通常是同一个类的不同配置 | 不同的类 |
| 复杂度 | 参数多、步骤多 | 参数简单 |
适用场景
| HTTP 请求构建 | RequestBuilder().url(…).method(…).header(…).build() |
| SQL 查询构建 | QueryBuilder().select(…).from(…).where(…).build() |
| 复杂对象创建 | 参数很多、有很多可选参数时 |
| 不可变对象 | 对象创建后不能修改 |
| 消息/邮件构建 | 构建包含多个字段的消息对象 |
优缺点
| 可读性好,代码清晰 | 需要额外创建 Builder 类 |
| 可以灵活控制创建过程 | 对简单对象来说过度设计 |
| 支持链式调用 | |
| 可以创建不可变对象 | |
| 参数校验更灵活 |
04 原型模式 Prototype
一句话理解
通过"复制"已有对象来创建新对象,而不是从零 new。
生活类比
你写了一份 Word 文档,觉得不错,Ctrl+C、Ctrl+V 复制了一份。这就是原型模式——从已有的"原型"复制出新对象。
核心思想
实现 Cloneable 接口 → 重写 clone() 方法 → 决定用浅拷贝还是深拷贝。
浅拷贝 vs 深拷贝
浅拷贝:
Level A → Monster ← Level B
↑
两个 Level 指向同一个 Monster!
改 A 的 Monster,B 也会变
深拷贝:
Level A → Monster A
Level B → Monster B (完全独立)
| 基本类型字段 | 复制值 | 复制值 |
| 引用类型字段 | 复制引用(指向同一对象) | 复制整个对象(完全独立) |
| 实现方式 | Object.clone() | 序列化/反序列化 |
| 安全性 | 不安全(共享引用) | 安全(完全独立) |
深拷贝的实现方式
- 递归 clone:容易遗漏嵌套的引用类型,不推荐
- 序列化/反序列化:一次性解决所有层级的拷贝,推荐
- JSON 转换:通过 JSON 序列化再反序列化实现深拷贝
真实应用案例
| Spring Bean 原型作用域 | scope="prototype" 每次获取都创建新实例 |
| Excel 复制工作表 | 复制整个工作表的结构和数据 |
| Word 文档模板 | 基于模板创建新文档,保留格式和内容 |
| 游戏中的技能特效 | 从技能模板复制出多个特效实例,微调参数 |
| 邮件模板 | 基于模板克隆出多封邮件,修改收件人和内容 |
| CAD 设计 | 复制已有组件,修改参数创建新组件 |
| 数据库备份 | 复制数据库结构和数据创建测试环境 |
| JVM 对象复制 | Object.clone() 的底层实现 |
| Protobuf 消息复制 | 深拷贝消息对象避免副作用 |
适用场景
| 游戏怪物生成 | 从模板复制大量怪物,微调属性 |
| 创建成本大 | 数据库连接、网络请求等创建很贵 |
| 大量相似对象 | 与其一个个 new,不如 clone 模板 |
| 保护性拷贝 | 防止外部修改内部状态 |
| 对象缓存 | 缓存对象的克隆版本 |
优缺点
| 避免重复的初始化操作 | 需要实现 Cloneable 接口 |
| 比 new 更快(尤其是复杂对象) | 深拷贝实现较复杂 |
| 简化对象创建过程 | 有循环引用时深拷贝会出问题 |
| 保护性拷贝防止数据泄露 | 需要处理 CloneNotSupportedException |
常见面试题
- 浅拷贝和深拷贝的区别? 浅拷贝只复制对象本身和基本类型字段,引用类型字段仍指向原对象;深拷贝把引用类型也复制一份,完全独立。
- 为什么推荐序列化方式做深拷贝? 递归 clone 容易遗漏嵌套的引用类型,序列化/反序列化一次性解决所有层级的拷贝。
- 原型模式和拷贝构造函数的区别? 原型模式通过 clone() 方法实现,拷贝构造函数通过构造方法实现。拷贝构造函数更灵活,可以处理 final 字段。
三、结构型模式(7种)— 关注"怎么组合类和对象"
结构型模式关注如何将类和对象组合成更大的结构,以实现新功能或简化设计。
05 适配器模式 Adapter
一句话理解
把一个类的接口转换成另一个类需要的接口——就像电源适配器。
生活类比
你从美国带回来一个插头(美标),在中国用不了。怎么办?买个转换头(适配器),插上就能用了。转换头没有改变插头本身,也没有改变墙上的插座,只是在中间做了"翻译"。
核心结构
目标接口(客户端期望的) ← 适配器 ← 被适配者(已有的、不兼容的)
两种适配器对比
| 对象适配器 | 用组合(持有被适配者引用) | 推荐,更灵活 |
| 类适配器 | 用继承(继承被适配者) | Java 只能继承一个类,不灵活 |
真实应用案例
| Java InputStreamReader | 将字节流 InputStream 适配为字符流 Reader |
| Java Arrays.asList() | 将数组适配为 List 接口 |
| Spring HandlerAdapter | 将不同类型的 Handler 适配为统一的处理接口 |
| Log4j 适配 Logback | 旧代码用 Log4j API,通过适配器切换到 Logback 实现 |
| 第三方 SDK 整合 | 将第三方 SDK 的接口适配为项目统一的接口 |
| 旧系统接口兼容 | 新系统调用旧系统的接口时,用适配器做转换 |
| USB-C 转接头 | 将 USB-C 接口适配为 USB-A、HDMI 等接口 |
| 数据格式转换 | 将 XML 数据适配为 JSON 接口 |
适用场景
| 整合第三方库 | 第三方库接口和你的不一样,写适配器转换 |
| 兼容旧系统 | 旧接口改名了,用适配器兼容 |
| 统一接口 | 多个不同接口的类,用适配器统一成一个接口 |
| 数据格式转换 | XML → JSON,List → Map |
| 框架整合 | Spring MVC 的 HandlerAdapter |
优缺点
| 复用已有类,不改源码 | 增加类的数量 |
| 符合开闭原则 | 适配器太多会很混乱 |
| 灵活(对象适配器) | 增加系统复杂度 |
| 降低耦合度 |
06 桥接模式 Bridge
一句话理解
把"抽象"和"实现"分开,让它们可以独立变化。
生活类比
遥控器(抽象)和电视(实现)是两个独立的东西。一个遥控器可以控制不同品牌的电视,一个电视也可以被不同遥控器控制。它们通过"信号"这座桥连接。
遥控器(抽象)──── 桥 ──── 电视(实现)
│ │
电视遥控器 海信电视
万能遥控器 小米电视
核心价值
如果不用桥接模式,要为每种存储方式写一个 Persistence 类:DbPersistence、FilePersistence……类爆炸。桥接后:N×M 变成 N+M。
核心结构
抽象层(Persistence) ──── 桥接 ──── 实现层(PersistenceImplementor)
│ │
高级操作 底层存储
saveData() DataBase
updateData() FileSystem
真实应用案例
| JDBC 驱动 | Driver 接口是抽象,各数据库厂商提供实现(MySQL、Oracle、PostgreSQL) |
| Java AWT | Shape 抽象和 Graphics 实现分离,不同平台有不同的图形实现 |
| 消息发送系统 | 消息类型(文本、图片、视频)是抽象,发送方式(短信、邮件、微信)是实现 |
| 跨平台应用 | 功能逻辑是抽象,平台实现(Windows、Mac、Linux)是实现 |
| 日志框架 | 日志级别(INFO、WARN、ERROR)是抽象,输出方式(控制台、文件、数据库)是实现 |
| 支付系统 | 支付方式(微信、支付宝)是抽象,支付场景(PC、移动端)是实现 |
| 游戏角色系统 | 角色类型(战士、法师)是抽象,武器类型(剑、法杖)是实现 |
适用场景
| 跨平台应用 | 抽象=功能,实现=平台(Windows/Mac) |
| 多种数据库 | 抽象=业务操作,实现=MySQL/Oracle |
| 消息发送 | 抽象=消息类型,实现=短信/邮件/微信 |
| 图形绘制 | 抽象=形状,实现=颜色 |
| 设备驱动 | 抽象=设备操作,实现=具体驱动 |
优缺点
| 避免类爆炸(N×M 变成 N+M) | 理解成本较高 |
| 符合开闭原则和单一职责 | 需要设计时就考虑好维度 |
| 抽象和实现可以独立扩展 | 增加系统复杂度 |
| 提高可扩展性 |
常见面试题
- 桥接模式和适配器模式的区别? 桥接模式是设计时就把抽象和实现分开;适配器模式是事后解决已有类的接口不兼容问题。
07 组合模式 Composite
一句话理解
把"单个对象"和"容器对象"统一处理——像文件和文件夹一样。
生活类比
你操作文件时,删一个文件和删一个文件夹,操作是一样的(右键→删除)。文件夹里可以有文件,也可以有其他文件夹——这就是树形结构。
核心结构
LetterComposite(抽象类)
├── children: List<LetterComposite> ← 持有子节点
├── add() ← 添加子节点
└── print() ← 统一操作(递归)
├── Letter(叶子节点,没有子节点)
└── Sentence(容器节点,包含 Letter)
关键设计
- 抽象组件:定义叶子和容器的公共接口,包含 add()、print() 等方法
- 叶子节点:实现自己的操作,没有子节点
- 容器节点:持有子节点列表,print() 方法递归调用所有子节点的 print()
- 钩子方法:printThisBefore() 和 printThisAfter() 提供扩展点
真实应用案例
| 文件系统 | 文件和文件夹统一操作(复制、删除、移动) |
| DOM 树 | HTML 元素和容器元素(div、ul)统一处理 |
| GUI 组件树 | JPanel 包含 JButton、JLabel,统一处理事件 |
| 菜单系统 | 菜单项和子菜单统一处理点击事件 |
| 组织架构 | 部门包含子部门和员工,统一查询和统计 |
| XML/JSON 解析 | 节点和叶子节点统一遍历和处理 |
| Java 集合框架 | Collection 接口定义了 add() 和 iterator() |
| Git 分支 | 目录和文件的树形结构管理 |
| UI 框架 | React/Vue 的组件树结构 |
适用场景
| 文件系统 | 文件和文件夹统一操作(复制、删除) |
| UI 组件树 | 按钮、面板、窗口的嵌套 |
| 菜单系统 | 菜单项和子菜单 |
| 组织架构 | 部门包含子部门和员工 |
| XML/JSON 解析 | 节点和叶子节点统一处理 |
优缺点
| 统一处理单个对象和组合对象 | 设计较复杂 |
| 新增节点类型容易 | 叶子节点不需要的方法也要实现 |
| 天然的树形结构 | 递归调用可能影响性能 |
| 简化客户端代码 |
08 装饰器模式 Decorator
一句话理解
不修改原有类,动态地给对象"穿上新功能"——像给手机贴膜、加壳。
生活类比
你买了一杯咖啡(基础对象),可以加糖、加奶、加冰——每加一种就是一个装饰器。加完之后还是咖啡,但功能更强了。
咖啡 → 加糖咖啡 → 加糖加奶咖啡 → 加糖加奶加冰咖啡
↑ ↑ ↑
基础 装饰器1 装饰器2
核心结构
Component(统一接口)
├── ConcreteComponent(被装饰者)
└── Decorator(装饰器基类,持有 Component 引用)
├── ConcreteDecoratorA(加攻击)
└── ConcreteDecoratorB(加防御)
关键设计
- 统一接口:被装饰者和装饰器实现同一个接口
- 装饰器基类:持有被装饰者的引用,默认行为是转发调用
- 具体装饰器:继承装饰器基类,重写方法增强功能
- 自由组合:多个装饰器可以嵌套使用
真实应用案例
| Java IO 流 | BufferedInputStream 包装 FileInputStream,DataInputStream 再包装 |
| Java Collections | Collections.unmodifiableList() 返回不可变的装饰器 |
| Java Servlet Filter | 每个 Filter 装饰 Request/Response,层层增强 |
| Spring AOP | AOP 本质上是装饰器模式的动态代理实现 |
| 日志增强 | 给已有方法加上日志记录,不修改原代码 |
| 权限校验 | 在方法执行前加权限检查 |
| 性能监控 | 在方法前后加耗时统计 |
| 缓存层 | 给数据访问加上缓存装饰器 |
| 加密/压缩 | GZIPInputStream 装饰 FileInputStream |
装饰器 vs 继承
| 灵活性 | 运行时自由组合 | 编译时固定 |
| 类数量 | 少 | 多(组合爆炸) |
| 符合原则 | 开闭原则 | 违反开闭原则 |
适用场景
| Java IO 流 | BufferedInputStream 包装 FileInputStream |
| 日志增强 | 给已有方法加上日志记录 |
| 权限校验 | 在方法执行前加权限检查 |
| 性能监控 | 在方法前后加耗时统计 |
| 缓存 | 给数据访问加上缓存层 |
优缺点
| 不修改原有代码 | 多层装饰时调试困难 |
| 可以自由组合功能 | 包装层数多了代码变复杂 |
| 符合开闭原则 | 会产生很多小类 |
| 运行时动态增强 |
09 外观模式 Facade
一句话理解
为复杂的子系统提供一个简单的"一键操作"入口。
生活类比
按一下电脑的开机键,CPU 启动、内存检测、硬盘加载、显示器点亮——你不需要一个一个手动操作,开机键就是"外观"。
核心结构
客户端 → Facade(外观类)→ 子系统A
→ 子系统B
→ 子系统C
客户端只需要调用 Facade,不需要知道子系统的细节。
真实应用案例
| Spring JdbcTemplate | 封装了获取连接、执行 SQL、处理异常、关闭资源的复杂流程 |
| SLF4J | 统一日志 API,屏蔽 Log4j、Logback 等底层实现 |
| Hibernate Session | 封装了 JDBC 的复杂操作 |
| Spring MVC DispatcherServlet | 统一处理 HTTP 请求,分发到不同的 Handler |
| 微服务网关 | 为多个微服务提供统一的入口 |
| 智能家居 | "回家模式"一键开启灯光、空调、窗帘 |
| ORM 框架 | MyBatis/Hibernate 封装数据库操作 |
| 文件上传 | 封装文件校验、存储、返回 URL 等复杂流程 |
适用场景
| 简化复杂 API | 把多个 API 调用封装成一个方法 |
| 分层架构 | 每层对外只暴露一个 Facade |
| 第三方库整合 | 封装第三方库的复杂调用 |
| 模块间解耦 | 模块通过 Facade 通信,不直接依赖 |
| 简化客户端使用 | 隐藏子系统复杂性 |
优缺点
| 简化调用方使用 | 不符合开闭原则(加子系统要改 Facade) |
| 解耦客户端和子系统 | 可能变成上帝类(God Class) |
| 提高安全性(隐藏内部细节) | 不利于子系统扩展 |
| 减少依赖关系 |
常见面试题
- 外观模式和代理模式的区别? 外观模式是简化复杂系统的调用;代理模式是控制对对象的访问(权限、缓存、延迟加载等)。
- 什么时候不用外观模式? 子系统本身就不复杂,或者客户端需要直接操作子系统的细节时。
10 代理模式 Proxy
一句话理解
给对象找一个"替身",通过替身来控制对原对象的访问。
生活类比
明星不会直接接通告,而是通过经纪人(代理)。经纪人会帮你过滤、审核、安排——你和明星之间多了一层控制。
三种代理对比
| 需要接口 | 是 | 是 | 否 |
| 实现方式 | 手动写代理类 | 反射 + InvocationHandler | 字节码生成 |
| 性能 | 最好 | 一般 | 较好 |
| 灵活性 | 低(每个接口都要写) | 高(一个代理处理所有方法) | 高 |
三种代理详解
静态代理:手动写一个代理类,实现与目标对象相同的接口。优点是简单直接;缺点是每个接口都要写一个代理类,很啰嗦。
JDK 动态代理(推荐):基于接口,运行时生成代理类。所有方法调用都会走到 invoke() 方法,可以在这里统一做权限校验、日志记录等。一个 Proxy 处理所有方法。
CGLIB 代理:基于继承,运行时生成子类。不需要接口,通过 Enhancer 创建目标类的子类并重写方法。Spring AOP 默认对有接口的类用 JDK 动态代理,对没有接口的类用 CGLIB。
真实应用案例
| Spring AOP | JDK/CGLIB 动态代理 | 实现日志、事务、权限等横切关注点 |
| Hibernate 延迟加载 | 静态代理 | 返回代理对象,访问时才查询数据库 |
| MyBatis Mapper | JDK 动态代理 | 接口方法调用代理对象执行 SQL |
| RPC 远程调用 | 动态代理 | 本地代理远程服务,屏蔽网络细节 |
| Nginx 反向代理 | 静态代理 | 代理后端服务器,负载均衡 |
| CDN | 静态代理 | 代理源站,缓存静态资源 |
| 数据库连接池 | 静态代理 | 代理数据库连接,管理连接复用 |
| 权限控制 | 动态代理 | 方法执行前检查用户权限 |
| 事务管理 | 动态代理 | 方法前后开启/提交事务 |
| 日志记录 | 动态代理 | 记录方法调用日志 |
适用场景
| 权限控制 | 方法执行前检查权限 |
| 日志记录 | 记录方法调用日志 |
| 事务管理 | 方法前后开启/提交事务 |
| 延迟加载 | 图片代理,点击时才加载 |
| 缓存 | 先查缓存,没有再查数据库 |
| RPC 远程调用 | 本地代理远程服务 |
优缺点
| 不修改原代码增加功能 | 增加了类的数量 |
| 符合开闭原则 | 请求处理变慢(多一层调用) |
| 控制访问权限 | JDK 动态代理需要接口 |
| 灵活性高 |
11 享元模式 Flyweight
一句话理解
把相同的部分共享复用,减少内存占用——像 String 常量池。
生活类比
游戏中有 1000 个哥布林,但它们的模型、贴图、动画都是一样的,只有位置不同。如果每个哥布林都加载一套模型,内存就爆了。享元模式的做法:1000 个哥布林共享同一个模型数据(内部状态),只有位置不同(外部状态)。
核心概念
| 内部状态 | 不随环境改变,可共享 | 哥布林的模型、贴图 |
| 外部状态 | 随环境改变,不可共享 | 哥布林的位置、血量 |
| 享元工厂 | 缓存已创建的对象,避免重复创建 | PotionFactory |
内存对比
不用享元:
100 个治疗药水 = 100 个 Potion 对象 → 占用 100 份内存
用享元:
100 个治疗药水 = 1 个 HealingPotion 对象(共享)+ 100 个引用 → 只占 1 份内存
真实应用案例
| String 常量池 | "hello" 只创建一次,多个引用共享同一个对象 |
| Integer 缓存 | -128~127 的 Integer 被缓存,超出范围才创建新对象 |
| Byte 缓存 | -128~127 的 Byte 被缓存 |
| 线程池 | 线程创建成本高,复用已有线程 |
| 数据库连接池 | 连接复用,避免频繁创建销毁 |
| 游戏粒子系统 | 大量相同特效共享一份资源 |
| 棋盘游戏 | 棋子对象共享(黑白两种,位置是外部状态) |
| 字符处理 | 处理大量字符时,相同字符共享对象 |
| 正则表达式 | Pattern 对象被缓存复用 |
| 字体渲染 | 相同字符的字形数据共享 |
与单例的区别
- 单例:一个类只有一个实例(如全局配置)
- 享元:一个类可以有多个实例,但相同的内部状态共享同一个对象(如 String 常量池中有多个不同的字符串对象)
适用场景
| String 常量池 | "hello" 只创建一次,多个引用共享 |
| Integer 缓存 | -128~127 的 Integer 被缓存 |
| 线程池 | 线程创建成本高,复用已有线程 |
| 数据库连接池 | 连接复用 |
| 游戏粒子系统 | 大量相同特效共享一份资源 |
优缺点
| 大幅减少内存使用 | 增加了系统复杂度 |
| 提高运行效率 | 外部状态需要额外管理 |
| 减少 GC 压力 | 不适合外部状态变化频繁的场景 |
| 共享资源节省成本 |
常见面试题
- 享元模式和单例模式的区别? 单例是一个类只有一个实例;享元是一个类可以有多个实例,但相同的内部状态共享同一个对象。
- String s = "abc" 和 String s = new String("abc") 的区别? 前者使用常量池(享元),后者每次都创建新对象。
四、行为型模式(11种)— 关注"对象之间怎么通信"
行为型模式关注对象之间的职责分配和通信方式,定义对象间的交互规则。
12 责任链模式 Chain of Responsibility
一句话理解
请求沿着一条链传递,每个节点决定"处理"还是"传递给下一个"。
生活类比
请假流程:实习生 → 组长 → 经理 → 总监 → CEO。你的请假请求沿着这条链传递,每个人只有权限处理一定天数的请假,超过了就交给上级。
请假 1 天 → 组长批准 ✓(结束)
请假 3 天 → 组长转经理 → 经理批准 ✓(结束)
请假 7 天 → 组长转经理 → 经理转总监 → 总监批准 ✓(结束)
核心结构
RequestHandler(处理者基类)
│
├── next(指向下一个处理者)
└── handleRequest()(处理或传递)
│
OrcSoldier → OrcOfficer → OrcCommander
关键设计
- 抽象处理者:持有下一个处理者的引用(链表结构),提供默认的传递逻辑
- 具体处理者:判断请求类型,能处理就处理,不能处理就调用 super.handleRequest() 传递
- 组装责任链:通过构造函数嵌套创建链 new A(new B(new C(null)))
真实应用案例
| Servlet Filter | javax.servlet.Filter 链,每个 Filter 处理或传递请求 |
| Spring Interceptor | Spring MVC 的拦截器链 |
| Netty ChannelPipeline | Netty 的处理器链,每个 Handler 处理或传递数据 |
| 审批系统 | 请假、报销、采购等审批流程 |
| 异常处理 | try-catch 链,每个 catch 处理特定类型的异常 |
| 日志级别 | DEBUG → INFO → WARN → ERROR 的过滤链 |
| 权限校验 | 多层权限检查(登录→角色→资源) |
| DOM 事件冒泡 | 事件从子元素向父元素传递 |
| Struts2 拦截器 | 拦截器栈执行前后处理 |
| OkHttp 拦截器 | 请求/响应的拦截器链 |
适用场景
| 审批流程 | 请假、报销、采购审批 |
| Web 过滤器 | Servlet Filter 链 |
| 异常处理 | try-catch 链 |
| 日志级别 | DEBUG → INFO → WARN → ERROR |
| 拦截器 | Spring Interceptor |
优缺点
| 请求发送者和处理者解耦 | 请求可能到达链尾也没人处理 |
| 可以灵活调整链的结构 | 链太长时性能下降 |
| 符合开闭原则 | 调试不方便 |
| 动态组合处理逻辑 |
13 命令模式 Command
一句话理解
把"操作"封装成对象——可以保存、撤销、排队执行。
生活类比
餐厅点单:你告诉服务员"来一份宫保鸡丁",服务员把你的需求写在菜单上(命令对象)。厨师拿到菜单才开始做。菜单可以排队、可以取消、可以记录今天做了什么菜。
核心结构
Invoker(触发者:按钮/遥控器)
│
▼
Command(命令对象:execute() + undo())
│
▼
Receiver(实际执行者:编辑器/电视)
关键设计
- 命令接口:定义 execute() 方法,可选定义 undo() 方法
- 具体命令:封装 Receiver 和具体操作,持有 Receiver 的引用
- 触发者(Invoker):持有 Command 引用,调用 command.execute()
- 撤销功能:命令对象中保存被删除的内容,undo() 时恢复
真实应用案例
| 文本编辑器 | Ctrl+Z 撤销、Ctrl+Y 重做 |
| Git 操作 | commit、revert、cherry-pick 都是命令对象 |
| 事务管理 | 多个操作封装为一个命令,全部成功才提交 |
| 任务队列 | 异步任务排队执行(线程池的 execute) |
| 宏命令 | 一键执行多个操作(如 Photoshop 的动作录制) |
| 遥控器 | 电视遥控器的每个按钮绑定一个命令 |
| 游戏操作 | 游戏中的技能释放、道具使用封装为命令 |
| 数据库事务 | 多条 SQL 封装为一个事务命令 |
| 撤销重做栈 | 编辑器的 Undo/Redo 栈 |
适用场景
| 撤销/重做 | Ctrl+Z / Ctrl+Y |
| 宏命令 | 一键执行多个操作 |
| 任务队列 | 异步任务排队执行 |
| 事务 | 操作全部成功才提交 |
| 遥控器 | 一个遥控器绑定多个命令 |
优缺点
| 支持撤销/重做 | 命令类数量增多 |
| 解耦调用者和执行者 | 简单操作过度设计 |
| 可以序列化命令(持久化) | |
| 支持队列和日志 |
14 迭代器模式 Iterator
一句话理解
提供一种统一的方式来遍历集合,不需要知道集合内部怎么存储。
生活类比
你用遥控器换台,只需要按"上一个"、“下一个”,不需要知道电视内部是用数组还是链表存储频道的。
核心结构
Collection(集合)
└── iterator() → 返回 Iterator
Iterator(迭代器)
├── hasNext() → 还有下一个吗?
├── next() → 获取下一个
└── remove() → 删除当前元素
自定义分页迭代器
笔记中实现了一个分页迭代器,支持:
- next() / previous():翻页
- curr():获取当前页
- goFirst() / goLast():跳到首页/末页
- goPage(int pi):跳到指定页
- pages():计算总页数
- setPageSize(int size):设置每页大小
真实应用案例
| Java 集合框架 | List、Set、Map 都实现了 Iterable 接口 |
| for-each 语法糖 | 本质是迭代器模式的语法简化 |
| 分页查询 | 数据库分页、列表分页 |
| ResultSet | JDBC 的结果集就是迭代器 |
| Stream API | Java 8 的流式操作底层基于迭代器 |
| 数据库游标 | 游标逐行遍历查询结果 |
| 文件逐行读取 | BufferedReader 的 readLine() |
| XML SAX 解析 | 逐个遍历 XML 节点 |
适用场景
| 分页查询 | 数据库分页、列表分页 |
| 遍历集合 | List、Set、Map 的遍历 |
| 树形遍历 | 文件系统目录遍历 |
| 数据库结果集 | ResultSet 就是迭代器 |
优缺点
| 遍历逻辑与集合解耦 | 增加了类的数量 |
| 统一的遍历接口 | 简单集合用 for-each 更方便 |
| 支持多种遍历方式 | |
| 简化集合接口 |
15 中介者模式 Mediator
一句话理解
用一个"中介"来协调多个对象之间的交互,避免它们直接互相引用。
生活类比
房产中介:买房的人和卖房的人不直接联系,都通过中介沟通。这样双方不需要知道对方的细节,中介负责协调。
没有中介:
买家 ←→ 卖家 (直接耦合)
有中介:
买家 ←→ 中介 ←→ 卖家 (通过中介解耦)
核心结构
ComponentA ──→ Mediator ←── ComponentB
│
ComponentC ────────┘
组件之间不直接通信,都通过 Mediator 转发。
关键设计
- 中介者接口:定义 triggerEvent() 方法,组件通过事件名通知中介者
- 组件基类:持有 Mediator 引用,变化时通知中介者
- 具体中介者:实现事件分发逻辑,协调组件间的交互
真实应用案例
| 聊天室 | 用户通过聊天室服务器通信,不直接互发消息 |
| 飞机塔台 | 飞机通过塔台协调起降,不直接通信 |
| MVC Controller | Controller 充当 Model 和 View 的中介 |
| MQ 消息队列 | 生产者和消费者通过消息队列解耦 |
| 微服务网关 | 服务之间通过网关通信 |
| 表单联动 | 多个输入框之间的联动(选择省份→更新城市列表) |
| GUI 事件总线 | 组件通过事件总线通信 |
| Actor 模型 | Actor 之间通过消息通信 |
适用场景
| 聊天室 | 用户通过聊天室服务器通信 |
| 飞机塔台 | 飞机通过塔台协调起降 |
| 表单验证 | 多个输入框之间的联动 |
| GUI 组件 | 按钮、文本框、下拉框联动 |
| MVC 框架 | Controller 充当 Model 和 View 的中介 |
优缺点
| 减少对象间的直接依赖 | 中介者可能变成上帝类 |
| 集中控制交互逻辑 | 中介者复杂时难以维护 |
| 符合迪米特法则 | 增加了中介者的复杂度 |
| 易于扩展新组件 |
16 观察者模式 Observer
一句话理解
一个对象状态变了,自动通知所有关注它的对象——像微信公众号推送。
生活类比
你关注了"人民日报"公众号。每次它发新文章,你都会收到推送通知。你不需要每天去看,公众号会主动告诉你。
公众号(Subject) 粉丝(Observer)
│ │
├── 发布文章 ──→ 自动推送给所有粉丝
│
├── 粉丝A
├── 粉丝B
└── 粉丝C
核心结构
Subject(被观察者)
├── observers: List<Observer>
├── addObserver()
├── removeObserver()
└── notifyAll() ← 状态变化时通知所有观察者
Observer(观察者)
└── update() ← 接收通知,更新自己
关键设计
- Subject:维护观察者列表,提供添加/移除方法,状态变化时遍历通知
- Observer:定义 update() 接口,接收通知并更新自己
- 松耦合:Subject 不知道 Observer 具体是谁,只知道它实现了 Observer 接口
- 游戏案例:角色使用技能时,根据阵营过滤观察者(敌方扣血,友方不受影响)
真实应用案例
| Java Swing 事件 | ActionListener 监听按钮点击事件 |
| Java beans PropertyChange | JavaBean 的属性变化监听 |
| Spring ApplicationEvent | Spring 的事件发布/监听机制 |
| Guava EventBus | Google 的事件总线 |
| RxJava | 响应式编程,基于观察者模式 |
| WebSocket | 服务端推送消息给所有订阅者 |
| 消息队列 | 发布/订阅模式 |
| 股票行情 | 股价变化通知多个客户端 |
| Vue/React 响应式 | 数据变化自动更新视图 |
| 日志框架 | 日志事件通知多个 Appender |
适用场景
| 事件监听 | 按钮点击事件、鼠标事件 |
| 消息推送 | 微信公众号、邮件通知 |
| 股票行情 | 股价变化通知多个客户端 |
| 数据绑定 | MVC 中 Model 变化通知 View |
| 监控系统 | 服务器状态变化通知运维 |
优缺点
| 松耦合(Subject 不知道 Observer 具体是谁) | 通知顺序不确定 |
| 支持广播通信 | 可能造成循环依赖 |
| 符合开闭原则(新增观察者不用改 Subject) | 观察者过多时通知效率低 |
| 运行时动态订阅 |
17 状态模式 State
一句话理解
对象在不同状态下有不同的行为——状态变了,行为也跟着变。
生活类比
游戏角色有多种状态:站立、行走、攻击、受伤、死亡。在"站立"状态下可以"行走";在"攻击"状态下会自动回到"站立";"死亡"状态下什么也不能做。
站立 ──→ 攻击 ──→ 站立
│ │
↓ ↓
行走 受伤 ──→ 死亡
核心结构
Context(上下文,持有当前 State)
│
▼
State(状态基类)
├── StandState(站立状态)
├── AttackState(攻击状态)
├── HurtState(受伤状态)
└── DeadState(死亡状态)
每个 State 知道自己该做什么,以及何时切换到下一个状态。
关键设计
- 状态基类:定义 onEnter()(进入状态时执行)和 execute()(执行状态行为)
- 上下文:持有当前 State 引用,changeState() 切换状态并触发 onEnter()
- 状态转换:由具体状态类决定(如攻击状态执行后自动切换到站立状态)
- 消除 if-else:不同状态的行为分散到各个状态类中,而不是用大量 if-else 判断
状态转换图
┌─────────────┐
│ StandState │
└──────┬──────┘
│ 攻击
┌──────▼──────┐
│ AttackState │
└──────┬──────┘
│ 被打
┌──────▼──────┐
│ HurtState │ ──→ DeadState(血量为 0 时)
└──────┬──────┘
│ 恢复
┌──────▼──────┐
│ StandState │
└─────────────┘
真实应用案例
| 游戏角色状态 | 站立、行走、攻击、受伤、死亡 |
| TCP 连接状态 | LISTEN、ESTABLISHED、CLOSE_WAIT、TIME_WAIT |
| 订单状态机 | 待支付→已支付→已发货→已签收→已完成 |
| 线程状态 | NEW→RUNNABLE→BLOCKED→WAITING→TERMINATED |
| 电梯状态 | 运行、停止、开门、关门 |
| 播放器状态 | 播放、暂停、停止、快进 |
| 审批流程 | 草稿→审核中→已通过→已拒绝 |
| 游戏角色 AI | 巡逻→追击→攻击→逃跑 |
| 工作流引擎 | 任务的不同状态和流转 |
适用场景
| 游戏角色状态 | 站立、行走、攻击、受伤、死亡 |
| TCP 连接状态 | LISTEN、ESTABLISHED、CLOSE_WAIT |
| 订单状态 | 待支付→已支付→已发货→已完成 |
| 线程状态 | NEW→RUNNABLE→BLOCKED→TERMINATED |
| 电梯状态 | 运行、停止、开门、关门 |
优缺点
| 状态转换逻辑清晰 | 状态类数量增多 |
| 符合开闭原则(新增状态不改已有代码) | 状态转换关系复杂时难维护 |
| 消除大量 if-else | |
| 状态转换规则集中管理 |
与策略模式的区别
- 状态模式:状态转换由状态对象自身决定(如攻击后自动回到站立)
- 策略模式:策略选择由客户端决定(如客户端选择用攻击策略还是防御策略)
18 策略模式 Strategy
一句话理解
定义一组可以互换的算法,让客户端在运行时选择用哪个。
生活类比
出行方式:去公司可以开车、坐公交、骑自行车。每种方式都是一个"策略",你可以根据天气、心情随时换。
出行策略接口
├── 开车策略(快但堵车)
├── 公交策略(慢但便宜)
└── 骑车策略(自由但累)
核心结构
Context(上下文,持有策略引用)
│
▼
Strategy(策略接口)
├── ConcreteStrategyA
├── ConcreteStrategyB
└── ConcreteStrategyC
运行时可以随时切换策略。
策略模式 vs if-else
| 扩展性 | 每次加新策略都要改已有代码 | 加新策略只需写一个新类 |
| 符合原则 | 违反开闭原则 | 符合开闭原则 |
| 可读性 | 策略多时代码很长 | 每个策略独立一个类 |
真实应用案例
| Java Comparator | Collections.sort(list, comparator) 通过不同的比较器实现不同排序 |
| Java ThreadPool | 不同的拒绝策略(AbortPolicy、CallerRunsPolicy 等) |
| Spring Resource | 不同的资源加载策略(classpath、file、url) |
| 支付方式 | 微信、支付宝、银行卡各自实现支付接口 |
| 折扣计算 | 满减、打折、会员价各自实现折扣接口 |
| 排序算法切换 | 冒泡、快排、归并按需选择 |
| 验证策略 | 手机号、邮箱、身份证不同验证逻辑 |
| 导航路线 | 最短路线、最快路线、最便宜路线 |
| 压缩算法 | ZIP、GZIP、Snappy 按需选择 |
适用场景
| 排序算法切换 | 冒泡、快排、归并按需选 |
| 支付方式 | 微信、支付宝、银行卡 |
| 折扣计算 | 满减、打折、会员价 |
| 验证策略 | 手机号、邮箱、身份证不同验证逻辑 |
| 导航路线 | 最短路线、最快路线、最便宜路线 |
优缺点
| 消除 if-else | 策略类数量增多 |
| 运行时切换算法 | 客户端需要知道有哪些策略 |
| 符合开闭原则 | |
| 算法复用 |
19 模板方法 Template Method
一句话理解
在父类中定义"算法骨架",子类填充具体步骤。
生活类比
泡茶和泡咖啡的流程是一样的:
步骤一样,但"冲泡"和"加配料"不同(茶用茶叶,咖啡用咖啡粉)。父类定义流程骨架,子类填充差异部分。
泡饮模板(父类):
烧水() ← 通用
冲泡() ← 抽象,子类实现
倒进杯子() ← 通用
加配料() ← 抽象,子类实现
泡茶(子类):冲泡=放茶叶,加配料=加柠檬
泡咖啡(子类):冲泡=放咖啡粉,加配料=加牛奶
核心结构
public abstract class AbstractClass {
// 模板方法:定义算法骨架(用 final 防止子类修改)
public final void templateMethod() {
step1(); // 固定步骤
step2(); // 可变步骤(抽象)
step3(); // 可变步骤(抽象)
hook(); // 钩子方法(可选覆盖)
}
private void step1() { /* 通用实现 */ }
protected abstract void step2(); // 子类必须实现
protected abstract void step3(); // 子类必须实现
protected void hook() { } // 钩子,子类可选覆盖
}
关键设计
- 模板方法:用 final 修饰,防止子类修改算法骨架
- 抽象方法:子类必须实现的步骤
- 钩子方法(Hook):有默认实现,子类可选覆盖,提供扩展点
- 旅行案例:performTrip() 定义了 goTransport() → day1/2/3() → backTransport() 的流程,通用步骤在父类实现,具体行程由子类决定
真实应用案例
| JUnit | setUp() → testXxx() → tearDown() |
| Servlet | doGet() / doPost() 由子类实现 |
| Spring JdbcTemplate | 模板封装了获取连接、执行 SQL、关闭资源的流程 |
| Spring Data JPA | SimpleJpaRepository 定义了 CRUD 的模板 |
| AbstractQueuedSynchronizer | AQS 定义了同步器的模板方法 |
| HttpServlet | service() 方法定义模板,doGet/doPost 由子类实现 |
| Java IO | InputStream 的 read() 方法是模板方法 |
| 游戏循环 | init() → update() → render() 的游戏主循环 |
经典应用
| JUnit | setUp() → testXxx() → tearDown() |
| Servlet | doGet() / doPost() |
| 数据库访问 | 连接() → 执行SQL() → 关闭() |
| Spring JdbcTemplate | 模板封装了获取连接、执行、关闭的流程 |
| 流水线 | 步骤固定,每步的具体实现不同 |
优缺点
| 代码复用(通用步骤写在父类) | 增加了类的数量 |
| 流程清晰,易维护 | 继承关系不够灵活 |
| 符合开闭原则 | 父类添加抽象方法会影响所有子类 |
| 钩子方法提供扩展点 |
20 访问者模式 Visitor
一句话理解
在不修改元素类的前提下,为它们添加新操作。
生活类比
体检:医生(访问者)拿着体检表去各个科室(元素)检查。每个科室接受检查,但科室本身不用改——改的是医生的检查方式。
体检报告(Visitor)去访问:
内科(Element)→ 接受体检
外科(Element)→ 接受体检
眼科(Element)→ 接受体检
核心结构
Visitor(访问者) → 定义对每种元素的操作
Visitable(可访问元素)→ 定义 accept(Visitor)
双重分派:
element.accept(visitor) ← 第一次分派:选择元素类型
└── visitor.visit(this) ← 第二次分派:选择访问者类型
关键设计
- 双重分派:element.accept(visitor) 调用 visitor.visit(this),根据 element 和 visitor 的实际类型决定调用哪个 visit() 方法
- 可访问元素:定义 accept(IVisitor visitor) 方法
- 访问者接口:为每种元素类型定义一个 visit() 方法
- 具体访问者:实现具体的访问逻辑(如生成报告)
真实应用案例
| 编译器 AST | 遍历语法树,做类型检查、代码生成 |
| Eclipse JDT | Java 编译器的 AST 访问 |
| XML DOM 遍历 | NodeVisitor 遍历 XML 节点 |
| 文件格式转换 | PDF→Word、XML→JSON |
| 报表生成 | 对不同数据类型做不同统计 |
| 代码分析工具 | SonarQube 分析代码结构 |
| AST 解释器 | 遍历 AST 执行计算 |
适用场景
| 编译器 AST | 遍历语法树,做类型检查、代码生成 |
| 文件格式转换 | PDF→Word、XML→JSON |
| 报表生成 | 对不同数据类型做不同统计 |
| 对象结构稳定 | 元素类型不常变,但操作常变 |
优缺点
| 新增操作不改元素类 | 新增元素类型要改所有 Visitor |
| 相关操作集中在一个访问者 | 双重分派理解成本高 |
| 符合开闭原则(针对操作) | 元素暴露内部细节给访问者 |
21 解释器模式 Interpreter
一句话理解
为一种语言定义文法,并建立解释器来解析和执行语句。
生活类比
计算器输入 12 + 34,计算器(解释器)解析这个表达式,理解"12"是数字、"+"是运算符、"34"是数字,然后计算出结果 46。
核心结构
Expression(表达式)
├── TerminalExpression (终结符:具体的数字/变量)
└── NonTerminalExpression (非终结符:组合表达式,如加法)
Context(上下文):存储待解析的字符串和解析结果
罗马数字解析器示例
笔记中实现了一个罗马数字解析器,从高位到低位依次解析:
- 千位表达式:M = 1000
- 百位表达式:C=100, CD=400, D=500, CM=900
- 十位表达式:X=10, XL=40, L=50, XC=90
- 个位表达式:I=1, IV=4, V=5, IX=9
解析 “MCMXCIV” → 1000 + 900 + 90 + 4 = 1994
抽象语法树(AST)
+
/ \\
* C
/ \\
A B
解释器遍历这棵树,计算 (A * B) + C
真实应用案例
| SQL 解析器 | 把 SQL 字符串解析成可执行的查询 |
| 正则表达式引擎 | 解析正则模式进行匹配 |
| SpEL | Spring 的表达式语言 |
| MVEL / OGNL | 表达式语言框架 |
| 数学公式引擎 | 计算器、Excel 公式 |
| SQL 注入防护 | 解析 SQL 语法防止注入 |
| DSL(领域特定语言) | 自定义语言的解析执行 |
| 配置文件解析 | 解析 XML/JSON/YAML 配置 |
| 模板引擎 | Thymeleaf、FreeMarker 的模板解析 |
| 编译器 | 词法分析、语法分析 |
适用场景
| SQL 解析 | 把 SQL 字符串解析成可执行的查询 |
| 正则表达式 | 解析正则模式进行匹配 |
| 数学表达式 | 计算器、公式引擎 |
| 配置文件 | 解析 XML/JSON 配置 |
| 编译器 | 词法分析、语法分析 |
优缺点
| 语法容易修改和扩展 | 复杂语法的解释器很难维护 |
| 每个文法规则是一个类 | 每条规则至少一个类,类数量爆炸 |
| 易于实现简单文法 | 执行效率低于直接解析 |
22 备忘录模式 Memento
一句话理解
保存对象的内部状态,以便日后恢复——就是"存档/读档"。
生活类比
玩游戏时的存档功能:打 Boss 前先存个档,打输了就读档重来。存档保存了游戏的完整状态(角色位置、血量、装备等)。
存档(Memento):保存棋盘当前状态
读档(Restore):恢复到之前的状态
核心角色
| Originator(发起人) | 要被保存/恢复的对象 | Chessboard(棋盘) |
| Memento(备忘录) | 保存状态的数据结构 | ChessboardMemento |
| Caretaker(管理者) | 管理存档的容器 | GameCore(用栈保存历史) |
关键设计
- 保存状态:Originator 的 save() 方法创建 Memento,深拷贝防止外部修改
- 恢复状态:Originator 的 restore() 方法从 Memento 读取状态
- 历史管理:Caretaker 用 Stack 保存多个 Memento,支持多次撤销
- 悔棋案例:每次落子前先存档(push),悔棋时读档(pop)
真实应用案例
| 文本编辑器 | Ctrl+Z 撤销、Ctrl+Y 重做 |
| 游戏存档 | 保存和恢复游戏进度 |
| 浏览器历史 | 前进/后退按钮 |
| Git 版本控制 | commit 保存快照,checkout 恢复 |
| 数据库事务 | 操作失败时 rollback 到之前的状态 |
| 虚拟机快照 | 保存虚拟机状态,随时恢复 |
| 编辑器历史 | IntelliJ IDEA 的 Local History |
| Photoshop 历史记录 | 多步撤销 |
| Excel 撤销 | 多步撤销操作 |
适用场景
| 游戏存档/读档 | 保存和恢复游戏进度 |
| Ctrl+Z 撤销 | 编辑器的撤销功能 |
| 数据库事务回滚 | 操作失败时恢复到之前的状态 |
| 浏览器前进/后退 | 保存历史页面状态 |
| 版本控制 | Git 的 commit/checkout |
优缺点
| 不暴露对象内部细节就能保存状态 | 消耗内存(存档越多越大) |
| 简化了 Originator 的代码 | 深拷贝实现复杂 |
| 符合单一职责和开闭原则 | |
| 保存/恢复操作简单 |
常见面试题
- 备忘录模式和序列化有什么区别? 序列化是把对象转成字节流用于持久化存储;备忘录模式是内存中保存状态快照用于恢复,关注点是"保存/恢复"这个行为模式。
- 备忘录模式和命令模式的区别? 命令模式的 undo() 方法需要调用备忘录来保存和恢复状态,两者经常配合使用。
五、模式之间的关系
| 工厂模式 → 单例 | 可以用单例模式实现工厂类 |
| 策略模式 ↔ 状态模式 | 都是替换行为,结构相似,区别在于状态转换的决策者 |
| 代理模式 ↔ 装饰器模式 | 都包装原对象,结构相似,区别在于目的(控制访问 vs 增强功能) |
| 模板方法 → 工厂方法 | 模板方法中用工厂方法创建子类对象 |
| 观察者模式 → 中介者模式 | 中介者模式中常用观察者来通知组件 |
| 组合模式 ↔ 迭代器模式 | 组合模式的树形结构常用迭代器来遍历 |
| 装饰器模式 ↔ 代理模式 | 装饰器侧重功能增强,代理侧重访问控制 |
| 命令模式 → 备忘录模式 | 命令模式的 undo 通常需要备忘录保存状态 |
| 责任链模式 ↔ 装饰器模式 | 都是链式处理,但责任链每个节点决定是否处理,装饰器层层增强 |
| 访问者模式 ↔ 组合模式 | 访问者模式常用于遍历组合模式的树形结构 |
六、模式速查表
| 单例 Singleton | 全局唯一实例 | private 构造、static、getInstance | 创建型 |
| 工厂 Factory | 通过工厂创建对象 | switch、接口、多态 | 创建型 |
| 建造者 Builder | 分步构建复杂对象 | 链式调用、Director、Builder | 创建型 |
| 原型 Prototype | 复制已有对象 | Cloneable、clone、深/浅拷贝 | 创建型 |
| 适配器 Adapter | 接口转换 | 包装、转换、兼容 | 结构型 |
| 桥接 Bridge | 抽象与实现分离 | 组合、N+M | 结构型 |
| 组合 Composite | 树形结构统一处理 | 递归、叶子+容器 | 结构型 |
| 装饰器 Decorator | 动态添加功能 | 包装、叠加、Java IO | 结构型 |
| 外观 Facade | 简化复杂系统入口 | Facade、封装 | 结构型 |
| 代理 Proxy | 控制对象访问 | 替身、权限、AOP | 结构型 |
| 享元 Flyweight | 共享细粒度对象 | 内部状态、外部状态、缓存 | 结构型 |
| 责任链 Chain of Responsibility | 请求沿链传递 | 链表、处理或传递 | 行为型 |
| 命令 Command | 操作封装成对象 | 撤销、排队、宏命令 | 行为型 |
| 迭代器 Iterator | 统一遍历集合 | hasNext、next、for-each | 行为型 |
| 中介者 Mediator | 协调多对象交互 | 解耦、集中控制 | 行为型 |
| 观察者 Observer | 状态变化自动通知 | 推送、订阅、松耦合 | 行为型 |
| 状态 State | 不同状态不同行为 | 状态机、if-else 消除 | 行为型 |
| 策略 Strategy | 可互换的算法 | 运行时切换、消除 if-else | 行为型 |
| 模板方法 Template Method | 父类定义骨架 | final、hook、子类填充 | 行为型 |
| 访问者 Visitor | 不改元素加新操作 | 双重分派、accept+visit | 行为型 |
| 解释器 Interpreter | 定义语言文法并解析 | AST、终结符、非终结符 | 行为型 |
| 备忘录 Memento | 保存/恢复状态 | 存档/读档、Originator+Caretaker | 行为型 |



