Java 设计模式综述:从六大原则到常见模式使用场景
欢迎你来读这篇博客,这篇博客主要是关于Java 设计模式综述。
其中包括了设计模式的核心思想、六大设计原则、常见设计模式分类、每种模式的适用场景,以及在 Java / Spring 体系中的实践理解。
序言
设计模式并不是为了把代码写得“更像面试题”,而是为了在复杂业务中降低变化带来的破坏。
很多人第一次学习设计模式时,会把它当成 23 种或 24 种固定招式来背:单例、工厂、代理、策略、观察者……但真正写项目时会发现,模式不是目的,控制变化才是目的。
一段代码刚写出来时,最重要的是能跑;业务开始变化后,最重要的是好改;系统开始变大后,最重要的是边界清晰、依赖稳定、扩展可控。设计模式就是在这个阶段开始发挥价值。
这篇博客不会给每一种设计模式都塞代码示例,否则很容易变成“代码八股套餐”。本文重点放在:
- 设计模式到底解决什么问题;
- 六大设计原则分别是什么意思;
- 常见设计模式如何分类;
- 每种设计模式适合用在什么场景;
- 在 Java / Spring 项目中如何理解这些模式。
正文
chapter 1:设计模式是什么
设计模式是软件开发中对常见设计问题的可复用解决方案。
它不是框架,也不是工具类,更不是某个固定模板。设计模式的本质是:在特定上下文中,用一种经过验证的结构来组织对象之间的关系,从而让系统更容易扩展、维护和复用。
一个设计模式通常会回答几个问题:
- 当前系统中哪个地方容易变化?
- 变化点应该被封装到哪里?
- 哪些对象应该依赖抽象,哪些对象负责具体实现?
- 新需求来了,是新增代码,还是修改旧代码?
- 对象之间是直接调用,还是通过中间层解耦?
简单说,设计模式不是“多写几层”,而是“把变化关进笼子里”。代码像城市规划,不能每次修路都把市中心炸了重建。
设计模式的价值
设计模式的价值主要体现在以下几个方面:
| 价值 | 说明 |
|---|---|
| 提升复用性 | 把通用解决方案抽象出来,避免重复造轮子 |
| 降低耦合 | 通过接口、抽象类、中间对象等方式减少直接依赖 |
| 提高扩展性 | 新增功能时尽量新增类,而不是修改大量旧代码 |
| 提升可读性 | 常见模式有统一命名,团队沟通成本更低 |
| 控制复杂度 | 把复杂逻辑拆解到合适的对象和层次中 |
设计模式不是银弹
设计模式也可能被滥用。
如果业务很简单,却硬套工厂、策略、代理、抽象工厂,就会让代码变得绕。最好的设计不是模式最多,而是刚好能承载当前复杂度,并为未来变化预留空间。
判断是否需要设计模式,可以看三个信号:
- 这个地方是否经常变化?
- 这里是否存在大量
if-else或switch? - 新增一种业务类型时,是否需要修改很多老代码?
如果答案经常是“是”,那设计模式可能就该登场了。
chapter 2:设计模式的六大原则
设计模式背后有一套更底层的思想,也就是常说的六大设计原则。
很多设计模式其实都是这些原则的具体落地。也就是说,原则是“道”,模式是“术”。只学模式不理解原则,就像只背招式不练内功,打起来容易变成花拳绣腿。
2.1 单一职责原则:Single Responsibility Principle
单一职责原则的核心思想是:一个类应该只有一个引起它变化的原因。
也就是说,一个类不要承担太多职责。职责越多,变化原因越多,类就越不稳定。
例如,一个订单类如果同时负责订单数据、价格计算、库存扣减、消息通知、日志记录、数据库保存,那么它迟早会变成“上帝类”。需求一改,牵一发动全身。
更合理的做法是:
| 职责 | 推荐拆分 |
|---|---|
| 订单数据 | Order |
| 价格计算 | PriceCalculator |
| 库存扣减 | InventoryService |
| 消息通知 | NotificationService |
| 持久化 | OrderRepository |
| 日志记录 | LogService / AOP |
单一职责原则不是要求每个类只有一个方法,而是要求它的职责边界清晰。
在 Java 后端项目中,Controller、Service、Repository、Assembler、Validator、Client 分层,本质上就是单一职责原则的体现。
2.2 开闭原则:Open-Closed Principle
开闭原则的核心思想是:软件实体应该对扩展开放,对修改关闭。
通俗来说,就是新需求来了,尽量通过新增代码实现,而不是频繁修改已经稳定的老代码。
例如支付系统中,最开始只有支付宝支付:
1 | |
后来增加微信支付、银行卡支付、PayPal 支付,如果一直追加 if-else,代码会越来越臃肿,也越来越容易出错。
更好的做法是抽象出 PaymentStrategy 接口,每一种支付方式实现一个策略类。新增支付方式时,只需要新增实现类,而不是修改核心支付流程。
开闭原则不是说永远不能修改旧代码,而是要让稳定的核心流程尽量少改,让变化的部分通过扩展点接入。
在 Spring 中,很多扩展机制都体现了开闭原则,比如:
- 自定义 BeanPostProcessor;
- 自定义 HandlerInterceptor;
- 自定义 Filter;
- 自定义 Converter;
- 自定义 Starter;
- 通过接口注入多种策略实现。
2.3 里氏替换原则:Liskov Substitution Principle
里氏替换原则的核心思想是:子类对象应该能够替换父类对象,并且程序行为不被破坏。
简单说,子类继承父类之后,不能破坏父类原本承诺的行为。
一个常见反例是“正方形继承长方形”。如果长方形有 setWidth() 和 setHeight(),正方形为了保持宽高一致,可能会修改这两个方法的行为。这样当程序把正方形当成长方形使用时,就会出现意外结果。
里氏替换原则提醒我们:
- 继承不是为了复用代码,而是为了表达“is-a”关系;
- 子类不要随意覆盖父类方法并改变语义;
- 如果子类不能稳定替换父类,应该优先考虑组合而不是继承。
在实际项目中,里氏替换原则经常用于判断抽象是否合理。
例如:
1 | |
如果 Bird 抽象了 fly() 方法,那么企鹅就很尴尬。此时说明抽象层级可能有问题,应该把“会飞”拆成独立能力,比如 Flyable。
2.4 接口隔离原则:Interface Segregation Principle
接口隔离原则的核心思想是:客户端不应该依赖它不需要的接口。
换句话说,接口要小而专,不要设计成一个巨大的“万能接口”。
例如:
1 | |
这个接口很容易膨胀。不同调用方可能只需要其中一部分能力,却被迫依赖整个接口。
更合理的方式是按场景拆分:
| 接口 | 职责 |
|---|---|
| UserCommandService | 用户创建、修改、删除 |
| UserQueryService | 用户查询 |
| UserImportService | 用户导入 |
| UserExportService | 用户导出 |
| PasswordService | 密码重置 |
| MarketingService | 营销触达 |
接口隔离原则在微服务、SDK、OpenAPI、RPC 接口设计中尤其重要。接口越胖,调用方依赖越重,后续演进成本越高。
2.5 依赖倒置原则:Dependency Inversion Principle
依赖倒置原则的核心思想是:
- 高层模块不应该依赖低层模块,二者都应该依赖抽象;
- 抽象不应该依赖细节,细节应该依赖抽象。
听起来有点绕,翻译成人话就是:业务核心不要直接依赖具体技术实现,而应该依赖接口。
例如订单服务不应该直接依赖 MySQLOrderRepository,而应该依赖 OrderRepository 接口。至于底层是 MySQL、PostgreSQL、Redis、ES,或者远程 RPC,对业务层来说都应该是细节。
这也是 DDD、Clean Architecture、Hexagonal Architecture 中非常重要的思想。
常见体现包括:
- Service 依赖 Repository 接口,而不是具体 DAO;
- 业务层依赖 MessagePublisher 接口,而不是直接依赖 KafkaTemplate;
- 领域层依赖 PaymentGateway 接口,而不是直接依赖某个支付 SDK;
- Controller 依赖应用服务接口,而不是直接操作数据库。
Spring 的依赖注入本质上就是依赖倒置原则的工程化实现。
2.6 迪米特法则:Law of Demeter
迪米特法则也叫最少知识原则,核心思想是:一个对象应该尽量少了解其他对象的内部细节。
通俗来说,就是“只和直接朋友说话,不要跨层扒别人家抽屉”。
例如:
1 | |
这种链式调用看起来很爽,但它暴露了太多内部结构。只要 Customer、Address、City 中任何一个结构变化,调用方都可能受到影响。
更好的方式是让对象自己提供行为:
1 | |
或者通过专门的查询服务、DTO、Assembler 来组织数据。
迪米特法则在实际项目中的体现包括:
- Controller 不直接操作 Entity 内部复杂结构;
- 外部系统调用通过 Client 封装;
- 复杂对象构建通过 Builder 或 Factory 封装;
- 多个服务协作通过 Facade 或 Application Service 统一编排;
- 聚合内部对象不随便暴露给外部修改。
迪米特法则不是要求对象之间完全不通信,而是要求通信边界更清楚。
2.7 补充:合成复用原则
虽然常见说法是六大原则,但有些资料也会把合成复用原则单独列出,形成“七大原则”。
合成复用原则的核心思想是:优先使用组合,而不是继承。
继承会把父类实现细节暴露给子类,耦合较强;组合则可以在运行时灵活替换依赖,更符合开闭原则和依赖倒置原则。
例如:
1 | |
在现代 Java 项目中,组合通常比继承更安全。继承像婚姻,组合像合作;前者牵一发动全家,后者边界清楚一点。
chapter 3:设计模式的分类
经典 GoF 设计模式通常分为三大类,共 23 种:
| 分类 | 关注点 | 常见模式 |
|---|---|---|
| 创建型模式 | 对象如何创建 | 单例、工厂方法、抽象工厂、建造者、原型 |
| 结构型模式 | 类和对象如何组合 | 适配器、桥接、组合、装饰器、外观、享元、代理 |
| 行为型模式 | 对象之间如何协作 | 责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者 |
有些中文资料会说“24 种设计模式”,通常是把简单工厂模式也算进去。严格来说,简单工厂不是 GoF 经典 23 种之一,但在 Java 后端项目中非常常见,所以本文也会一起介绍。
下面用一张 Mermaid 图快速总览:
mindmap
root((设计模式))
六大原则
单一职责原则
开闭原则
里氏替换原则
接口隔离原则
依赖倒置原则
迪米特法则
创建型模式
简单工厂
工厂方法
抽象工厂
单例
建造者
原型
结构型模式
适配器
桥接
组合
装饰器
外观
享元
代理
行为型模式
责任链
命令
解释器
迭代器
中介者
备忘录
观察者
状态
策略
模板方法
访问者
chapter 4:创建型模式
创建型模式关注的是对象创建过程。
它们解决的核心问题是:对象怎么创建、由谁创建、创建过程如何隐藏、复杂对象如何组装。
当系统中对象创建逻辑变复杂,或者你不希望业务代码直接依赖具体实现类时,就可以考虑创建型模式。
4.1 简单工厂模式
简单工厂模式通过一个工厂类,根据不同参数返回不同对象。
它不是 GoF 正式设计模式之一,但非常常见。
| 维度 | 说明 |
|---|---|
| 核心思想 | 把对象创建逻辑集中到一个工厂类中 |
| 解决问题 | 避免调用方直接 new 具体实现类 |
| 适用场景 | 对象类型较少,创建逻辑不复杂 |
| 常见例子 | 根据支付类型创建支付处理器;根据文件类型创建解析器 |
| 注意事项 | 类型变多后,工厂类容易膨胀,可能违反开闭原则 |
适合场景:
- 根据枚举创建不同处理器;
- 根据消息类型创建不同消息解析器;
- 根据渠道类型创建不同渠道客户端;
- 业务简单,没必要引入复杂工厂结构。
简单工厂适合小规模场景。对象种类一旦快速增长,就要考虑工厂方法或策略模式。
4.2 工厂方法模式
工厂方法模式把对象创建延迟到子类中完成。
它的核心思想是:父类定义创建对象的接口,子类决定具体创建哪一种对象。
| 维度 | 说明 |
|---|---|
| 核心思想 | 一个产品对应一个工厂,具体工厂负责创建具体产品 |
| 解决问题 | 避免简单工厂中大量 if-else |
| 适用场景 | 产品类型经常扩展,创建逻辑需要解耦 |
| 常见例子 | 日志框架创建不同 Logger;数据库驱动创建不同连接 |
| 注意事项 | 类数量会增加,结构比简单工厂更复杂 |
适合场景:
- 产品种类会持续增加;
- 每种产品创建逻辑不同;
- 希望新增产品时不修改已有工厂;
- 希望调用方只依赖抽象产品。
在 Java 中,FactoryBean、BeanFactory、LoggerFactory 等都能看到工厂思想。
4.3 抽象工厂模式
抽象工厂模式用于创建一组相关或相互依赖的对象。
工厂方法关注“一个产品等级结构”,抽象工厂关注“一整个产品族”。
| 维度 | 说明 |
|---|---|
| 核心思想 | 一个工厂创建一组相关产品 |
| 解决问题 | 保证同一产品族对象之间的一致性 |
| 适用场景 | 系统需要切换一整套产品组合 |
| 常见例子 | 不同数据库方言组件;不同操作系统 UI 组件;不同云厂商 SDK 适配 |
| 注意事项 | 扩展新产品族容易,扩展新产品等级较麻烦 |
适合场景:
- 一套主题 UI:按钮、输入框、弹窗都要保持同一风格;
- 一套云服务:OSS、短信、队列都来自同一云厂商;
- 一套数据库适配:连接、方言、分页、SQL 生成器都需要配套;
- 多租户系统中,不同租户有整套不同业务实现。
抽象工厂适合“成套切换”的场景。如果只是单个对象变化,用工厂方法或策略模式通常更轻。
4.4 单例模式
单例模式保证一个类在系统中只有一个实例,并提供全局访问点。
| 维度 | 说明 |
|---|---|
| 核心思想 | 控制实例数量,保证全局唯一 |
| 解决问题 | 避免重复创建昂贵对象或状态冲突 |
| 适用场景 | 配置中心、线程池、缓存管理器、连接池管理器 |
| 常见例子 | Spring Bean 默认单例;Runtime;日志对象 |
| 注意事项 | 注意线程安全、反射破坏、序列化破坏、全局状态污染 |
适合场景:
- 对象创建成本较高;
- 需要全局共享状态;
- 需要统一管理资源;
- 系统中天然只应该存在一个实例。
不过在 Spring 项目中,很多时候不需要手写单例。Spring 容器默认管理的 Bean 就是单例作用域。与其自己写双重检查锁,不如交给容器,少一点手搓,少一点玄学 bug。
4.5 建造者模式
建造者模式用于一步一步构建复杂对象。
它的核心思想是:把复杂对象的构建过程和最终表示分离。
| 维度 | 说明 |
|---|---|
| 核心思想 | 分步骤构建复杂对象 |
| 解决问题 | 构造函数参数过多、可选参数过多、对象创建过程复杂 |
| 适用场景 | DTO、配置对象、请求对象、复杂聚合对象 |
| 常见例子 | StringBuilder;Lombok @Builder;MyBatis 查询条件构造器 |
| 注意事项 | 简单对象没必要使用 Builder,否则会显得啰嗦 |
适合场景:
- 构造参数很多;
- 参数有大量可选项;
- 对象需要链式构建;
- 创建过程需要校验;
- 希望对象创建后不可变。
在 Java 中,Builder 特别适合构建请求参数对象、配置对象、测试数据对象、复杂 DTO。
4.6 原型模式
原型模式通过复制已有对象来创建新对象。
| 维度 | 说明 |
|---|---|
| 核心思想 | 通过克隆已有对象创建新对象 |
| 解决问题 | 创建对象成本高,或者对象初始化过程复杂 |
| 适用场景 | 模板对象复制、复杂配置复制、游戏对象复制 |
| 常见例子 | Java Cloneable;对象深拷贝;配置模板复制 |
| 注意事项 | 深拷贝和浅拷贝要区分清楚 |
适合场景:
- 对象创建成本很高;
- 同类对象差异很小;
- 初始化过程复杂;
- 需要复制模板并局部修改。
在业务系统中,原型模式常见于“复制一份配置”“基于模板创建新流程”“复制表单模板”等场景。
chapter 5:结构型模式
结构型模式关注的是类和对象之间如何组合。
它们解决的核心问题是:已有对象如何组织成更大的结构,并保持灵活性。
当系统里出现接口不兼容、对象层级复杂、功能需要动态增强、外部系统需要统一封装时,就可以考虑结构型模式。
5.1 适配器模式
适配器模式用于把一个类的接口转换成客户端期望的另一个接口。
| 维度 | 说明 |
|---|---|
| 核心思想 | 接口转换,让原本不兼容的对象可以协同工作 |
| 解决问题 | 老接口、新接口、第三方接口不一致 |
| 适用场景 | 第三方 SDK 接入、旧系统改造、多数据源适配 |
| 常见例子 | Spring MVC HandlerAdapter;InputStreamReader |
| 注意事项 | 适配器过多可能说明系统边界混乱 |
适合场景:
- 接入第三方支付 SDK;
- 对接不同短信供应商;
- 老系统接口不想大改;
- 新旧接口需要兼容;
- 多个外部系统返回格式不同,需要统一内部模型。
适配器模式非常适合“外部世界很乱,内部世界要稳”的场景。
5.2 桥接模式
桥接模式用于将抽象部分和实现部分分离,使它们可以独立变化。
| 维度 | 说明 |
|---|---|
| 核心思想 | 抽象维度和实现维度分离 |
| 解决问题 | 多维度变化导致类爆炸 |
| 适用场景 | 渠道 × 消息类型、平台 × 功能、形状 × 颜色 |
| 常见例子 | JDBC 驱动模型;日志框架抽象 |
| 注意事项 | 需要先识别出两个独立变化维度 |
适合场景:
- 消息通知系统:短信、邮件、微信 × 普通消息、营销消息、验证码消息;
- 文件导出系统:PDF、Excel、CSV × 本地、OSS、SFTP;
- 支付系统:支付渠道 × 支付方式;
- 多平台客户端:Windows、Mac、Linux × 不同功能模块。
如果你发现类名开始变成 AliPayQrCodePaymentService、WechatPayQrCodePaymentService、AliPayH5PaymentService,就要小心类爆炸问题了。
5.3 组合模式
组合模式用于把对象组织成树形结构,让客户端可以统一处理单个对象和组合对象。
| 维度 | 说明 |
|---|---|
| 核心思想 | 单个对象和组合对象具有一致接口 |
| 解决问题 | 树形结构处理复杂 |
| 适用场景 | 菜单树、组织架构、文件目录、权限树 |
| 常见例子 | Java Swing 组件树;文件系统目录结构 |
| 注意事项 | 树结构层级过深时要注意性能和递归问题 |
适合场景:
- RBAC 权限菜单;
- 部门组织架构;
- 评论嵌套回复;
- 商品分类树;
- 文件夹和文件;
- 工作流节点树。
组合模式的重点是让调用方不用关心处理的是单个节点还是一组节点。
5.4 装饰器模式
装饰器模式用于在不改变原对象的前提下,动态增强对象功能。
| 维度 | 说明 |
|---|---|
| 核心思想 | 用包装对象增强原对象 |
| 解决问题 | 不修改原类,又想叠加新能力 |
| 适用场景 | IO 流增强、请求包装、日志增强、缓存增强 |
| 常见例子 | Java IO;HttpServletRequestWrapper |
| 注意事项 | 装饰层数过多会增加理解成本 |
适合场景:
- 给原有服务增加缓存;
- 给请求增加额外上下文;
- 给流增加缓冲、压缩、加密能力;
- 给消息发送增加日志、重试、监控;
- 给接口调用增加限流、熔断、埋点。
装饰器模式强调“增强能力”,代理模式更强调“控制访问”。两者很像,但关注点不同。
5.5 外观模式
外观模式为复杂子系统提供一个统一入口。
| 维度 | 说明 |
|---|---|
| 核心思想 | 用统一门面屏蔽内部复杂性 |
| 解决问题 | 调用方需要理解太多内部服务 |
| 适用场景 | 复杂业务编排、第三方系统封装、应用服务层 |
| 常见例子 | Facade Service;统一网关;SDK Client |
| 注意事项 | Facade 不应变成新的上帝类 |
适合场景:
- 下单流程:校验、库存、优惠、支付、消息、积分;
- 结算流程:账单生成、费用计算、单据流转、通知;
- 第三方 SDK:多个底层 API 封装成一个 Client;
- 微服务聚合接口:多个服务调用聚合成统一响应;
- 复杂领域逻辑对外只暴露简洁接口。
在 DDD 应用层中,Application Service 经常承担外观模式的角色。
5.6 享元模式
享元模式用于共享大量细粒度对象,减少内存占用。
| 维度 | 说明 |
|---|---|
| 核心思想 | 共享可复用对象,区分内部状态和外部状态 |
| 解决问题 | 大量重复对象导致内存浪费 |
| 适用场景 | 缓存池、连接池、字符串常量池、对象池 |
| 常见例子 | Java String 常量池;Integer 缓存;线程池 |
| 注意事项 | 需要清楚哪些状态可以共享,哪些不能共享 |
适合场景:
- 大量重复字符串;
- 大量相同配置对象;
- 图形编辑器中重复图元;
- 游戏中的大量相似对象;
- 权限点、字典值、元数据缓存。
享元模式不是简单缓存。它强调共享对象内部状态,把变化的外部状态交给调用方传入。
5.7 代理模式
代理模式为目标对象提供一个代理对象,由代理对象控制对目标对象的访问。
| 维度 | 说明 |
|---|---|
| 核心思想 | 通过代理对象间接访问目标对象 |
| 解决问题 | 在访问目标对象前后加入控制逻辑 |
| 适用场景 | AOP、远程调用、权限控制、延迟加载、事务 |
| 常见例子 | Spring AOP;JDK 动态代理;CGLIB;MyBatis Mapper |
| 注意事项 | 代理可能带来调试复杂度和调用链隐藏问题 |
适合场景:
- 方法调用前做权限校验;
- 方法调用前后记录日志;
- 远程服务本地代理;
- 数据库事务代理;
- 延迟加载大对象;
- RPC Stub;
- 接口耗时监控。
Spring AOP 是代理模式的经典应用。你调用的是 Bean,看起来像普通方法,实际可能已经被代理对象层层包裹。Spring:你以为你在调用对象,其实你在调用“套娃”。
chapter 6:行为型模式
行为型模式关注的是对象之间如何通信、协作和分配职责。
它们解决的核心问题是:流程如何组织、行为如何切换、对象之间如何解耦。
当系统中出现复杂流程、大量分支判断、对象关系网混乱、状态变化复杂时,就可以考虑行为型模式。
6.1 责任链模式
责任链模式让多个处理器按顺序处理请求,直到某个处理器处理完成,或者整个链路执行结束。
| 维度 | 说明 |
|---|---|
| 核心思想 | 多个处理节点形成链,请求沿链传递 |
| 解决问题 | 多个处理步骤解耦 |
| 适用场景 | 过滤器、审批流、风控规则、校验链 |
| 常见例子 | Servlet FilterChain;Spring Security Filter Chain |
| 注意事项 | 链路过长时要注意排查困难和性能问题 |
适合场景:
- 登录认证流程;
- 参数校验链;
- 风控规则链;
- 审批流程;
- 网关过滤器;
- 订单处理流水线。
责任链模式非常适合把一长串 if-else 处理流程拆成多个独立节点。
6.2 命令模式
命令模式把请求封装成对象,从而支持排队、记录、撤销、重试等操作。
| 维度 | 说明 |
|---|---|
| 核心思想 | 把操作封装成命令对象 |
| 解决问题 | 请求发送者和执行者解耦 |
| 适用场景 | 任务队列、操作撤销、异步命令、工作流 |
| 常见例子 | Runnable;Command Bus;消息队列任务 |
| 注意事项 | 命令对象过多时需要良好命名和组织 |
适合场景:
- 异步任务提交;
- MQ 消息消费;
- 定时任务执行;
- 操作撤销和重做;
- 工作流节点执行;
- 批量任务排队;
- 游戏操作指令。
命令模式把“我要做什么”封装成对象,让执行时机和执行者都可以灵活变化。
6.3 解释器模式
解释器模式用于定义一种语言的语法,并解释执行语句。
| 维度 | 说明 |
|---|---|
| 核心思想 | 为特定语法定义解释规则 |
| 解决问题 | 特定表达式、规则、脚本需要解释执行 |
| 适用场景 | 规则引擎、表达式解析、SQL 解析、模板语法 |
| 常见例子 | SpEL;正则表达式;SQL Parser |
| 注意事项 | 复杂语法不建议手写解释器,应使用成熟解析器框架 |
适合场景:
- 营销规则表达式;
- 权限表达式;
- 动态计算公式;
- 配置化规则;
- DSL 解析;
- 模板变量替换。
解释器模式在普通业务代码中不算常见,但在规则引擎、表达式引擎、低代码平台中很重要。
6.4 迭代器模式
迭代器模式提供一种顺序访问集合元素的方式,而不暴露集合内部结构。
| 维度 | 说明 |
|---|---|
| 核心思想 | 用统一方式遍历集合 |
| 解决问题 | 遍历逻辑和集合内部结构解耦 |
| 适用场景 | 集合遍历、分页遍历、树遍历、流式处理 |
| 常见例子 | Java Iterator;Iterable;Stream |
| 注意事项 | 遍历过程中修改集合要注意并发修改问题 |
适合场景:
- 遍历 List、Set、Map;
- 批量分页处理数据;
- 自定义集合结构;
- 树形结构遍历;
- 文件流、数据流处理。
在 Java 中,迭代器模式已经非常基础,很多人每天都在用,只是没意识到。
6.5 中介者模式
中介者模式通过一个中介对象封装多个对象之间的交互,避免对象之间直接互相依赖。
| 维度 | 说明 |
|---|---|
| 核心思想 | 多个对象不直接互调,而是通过中介者协作 |
| 解决问题 | 对象关系网复杂、互相调用混乱 |
| 适用场景 | 聊天室、事件中心、UI 组件协作、工作流调度 |
| 常见例子 | 消息总线;事件总线;Controller 协调多个组件 |
| 注意事项 | 中介者容易膨胀成上帝对象 |
适合场景:
- IM 聊天室消息转发;
- 多组件 UI 联动;
- 多服务流程编排;
- 工作流节点调度;
- 游戏房间内玩家互动;
- 事件总线统一协调。
中介者模式的价值是减少对象之间的网状依赖,把复杂交互集中管理。
6.6 备忘录模式
备忘录模式用于在不破坏对象封装的前提下,保存对象的某个历史状态,并在需要时恢复。
| 维度 | 说明 |
|---|---|
| 核心思想 | 保存和恢复对象状态 |
| 解决问题 | 撤销、回滚、历史快照 |
| 适用场景 | 编辑器撤销、游戏存档、流程状态恢复 |
| 常见例子 | Undo/Redo;快照;版本记录 |
| 注意事项 | 状态对象过大时会带来存储压力 |
适合场景:
- 文本编辑器撤销;
- 表单草稿保存;
- 游戏进度存档;
- 工作流状态快照;
- 配置版本回滚;
- 数据修复前备份。
备忘录模式要特别注意状态大小。如果每次都保存完整对象,可能会变成“内存刺客”。
6.7 观察者模式
观察者模式定义对象之间的一对多依赖关系,当一个对象状态变化时,所有依赖者都会收到通知。
| 维度 | 说明 |
|---|---|
| 核心思想 | 发布订阅,状态变化自动通知 |
| 解决问题 | 事件发送者和接收者解耦 |
| 适用场景 | 事件驱动、消息通知、缓存刷新、领域事件 |
| 常见例子 | Spring ApplicationEvent;MQ;监听器 |
| 注意事项 | 异步事件要注意最终一致性和失败补偿 |
适合场景:
- 用户注册后发送欢迎短信、初始化积分、记录日志;
- 订单支付成功后通知库存、物流、积分、消息系统;
- 配置变更后刷新缓存;
- 领域事件发布;
- 前端事件监听;
- MQ 发布订阅。
观察者模式是事件驱动架构的基础思想之一。它能让主流程保持简洁,把副作用拆出去。
6.8 状态模式
状态模式允许对象在内部状态变化时改变其行为。
| 维度 | 说明 |
|---|---|
| 核心思想 | 不同状态对应不同行为 |
| 解决问题 | 状态判断逻辑复杂,大量 if-else |
| 适用场景 | 订单状态机、审批状态、工单状态、游戏角色状态 |
| 常见例子 | 状态机;Spring Statemachine;订单生命周期 |
| 注意事项 | 状态流转规则必须清晰,否则会更加混乱 |
适合场景:
- 订单:待支付、已支付、已发货、已完成、已取消;
- 工单:待处理、处理中、已解决、已关闭;
- 审批:草稿、审批中、通过、驳回;
- 会员:普通、VIP、冻结、注销;
- 任务:待执行、执行中、成功、失败。
状态模式适合状态少但行为差异明显的系统。如果状态和流转非常复杂,可以考虑引入状态机框架。
6.9 策略模式
策略模式定义一组算法或业务规则,并让它们可以互相替换。
| 维度 | 说明 |
|---|---|
| 核心思想 | 把可变算法封装成独立策略 |
| 解决问题 | 多种算法分支、大量 if-else |
| 适用场景 | 支付方式、优惠计算、排序算法、风控规则 |
| 常见例子 | Comparator;Spring 中多实现注入;支付策略 |
| 注意事项 | 策略选择逻辑也要管理好,避免转移复杂度 |
适合场景:
- 不同支付渠道;
- 不同优惠规则;
- 不同结算规则;
- 不同导出格式;
- 不同风控策略;
- 不同路由策略;
- 不同推荐算法。
策略模式是 Java 后端最常用的设计模式之一。很多业务代码里又臭又长的 if-else,本质上都可以用策略模式优化。
6.10 模板方法模式
模板方法模式在父类中定义算法骨架,把某些步骤延迟到子类实现。
| 维度 | 说明 |
|---|---|
| 核心思想 | 固定流程骨架,开放局部步骤 |
| 解决问题 | 多个流程大体相同,部分步骤不同 |
| 适用场景 | 数据导入、文件解析、任务执行、请求处理 |
| 常见例子 | JdbcTemplate;RestTemplate;AbstractApplicationContext |
| 注意事项 | 继承层级过深会降低灵活性 |
适合场景:
- 文件导入流程:读取、校验、转换、保存、报告;
- 支付流程:参数校验、调用渠道、处理结果、记录流水;
- 数据同步流程:拉取、转换、落库、通知;
- 定时任务执行框架;
- 抽象测试基类。
模板方法适合流程稳定、步骤可变的场景。它和策略模式经常搭配使用:模板负责流程,策略负责变化点。
6.11 访问者模式
访问者模式用于在不改变对象结构的前提下,为对象结构增加新的操作。
| 维度 | 说明 |
|---|---|
| 核心思想 | 把操作从对象结构中分离出来 |
| 解决问题 | 对稳定对象结构增加多种新操作 |
| 适用场景 | AST 解析、编译器、报表统计、复杂对象遍历 |
| 常见例子 | 编译器语法树访问;文件系统扫描 |
| 注意事项 | 对象结构变化频繁时不适合访问者模式 |
适合场景:
- 抽象语法树分析;
- 不同类型节点生成不同报表;
- 文件系统扫描并执行不同操作;
- 复杂对象结构导出;
- 对一组稳定类增加多种处理逻辑。
访问者模式相对高级,也相对难读。普通业务系统里不要轻易上来就用,除非对象结构稳定,而操作经常扩展。
chapter 7:设计模式在 Spring 中的体现
Spring 本身就是设计模式大型实践现场。
理解 Spring 中的设计模式,不是为了背源码,而是为了知道框架为什么这样设计。
| Spring 场景 | 对应模式 | 说明 |
|---|---|---|
| BeanFactory / ApplicationContext | 工厂模式 | 容器负责创建和管理 Bean |
| Spring Bean 默认作用域 | 单例模式 | 默认 Bean 在容器中只有一个实例 |
| Spring AOP | 代理模式 | 通过代理对象增强方法调用 |
| JdbcTemplate | 模板方法模式 | 固定 JDBC 操作流程,开放 SQL 和结果映射 |
| ApplicationEvent | 观察者模式 | 事件发布与监听解耦 |
| HandlerAdapter | 适配器模式 | 适配不同类型的 Controller 调用方式 |
| FilterChain | 责任链模式 | 请求经过多个过滤器处理 |
| HandlerInterceptor | 责任链思想 | 多个拦截器按顺序执行 |
| BeanPostProcessor | 扩展点 / 责任链思想 | Bean 初始化前后增强 |
| FactoryBean | 工厂模式 | 自定义复杂 Bean 创建逻辑 |
| Environment / PropertySource | 策略与组合思想 | 多来源配置统一管理 |
| Resource 抽象 | 策略 / 适配器思想 | 统一访问 classpath、file、URL 等资源 |
Spring 给我们的启发
Spring 中大量使用接口、抽象、回调、代理、模板、事件机制,本质上都是为了做到:
- 核心流程稳定;
- 扩展点开放;
- 具体实现可替换;
- 框架代码不依赖业务细节;
- 业务代码不感知底层复杂性。
这也是设计模式在真实工程中的价值。
chapter 8:如何在业务代码中选择设计模式
设计模式不应该靠背,而应该靠识别问题。
下面是一些常见问题和对应模式:
| 代码问题 | 可以考虑的模式 |
|---|---|
大量 if-else 根据类型执行不同逻辑 |
策略模式、状态模式、工厂模式 |
| 对象创建过程复杂 | 建造者模式、工厂模式 |
| 需要创建一组相关对象 | 抽象工厂模式 |
| 流程固定,部分步骤变化 | 模板方法模式 |
| 多个处理器按顺序执行 | 责任链模式 |
| 主流程完成后触发多个后续动作 | 观察者模式 |
| 接入第三方接口格式不一致 | 适配器模式 |
| 需要屏蔽复杂子系统 | 外观模式 |
| 方法调用前后要加日志、事务、权限 | 代理模式、装饰器模式 |
| 状态不同,行为不同 | 状态模式 |
| 树形结构需要统一处理 | 组合模式 |
| 大量重复对象占用内存 | 享元模式 |
| 需要撤销、回滚、恢复历史状态 | 备忘录模式 |
| 操作需要排队、异步、重试、撤销 | 命令模式 |
一个实用判断流程
可以按下面流程判断:
flowchart TD
A[发现代码变化频繁] --> B{变化点是什么}
B -->|创建对象复杂| C[考虑工厂 / 建造者 / 原型]
B -->|算法或规则变化| D[考虑策略]
B -->|状态驱动行为| E[考虑状态模式]
B -->|流程步骤固定| F[考虑模板方法]
B -->|多个节点依次处理| G[考虑责任链]
B -->|调用后触发副作用| H[考虑观察者]
B -->|接口不兼容| I[考虑适配器]
B -->|需要增强访问| J[考虑代理 / 装饰器]
B -->|子系统复杂| K[考虑外观]
真正写代码时,不需要一开始就套模式。可以先写出清晰直接的代码,然后在变化点出现时重构成模式。
设计模式最好的使用时机,往往不是第一版,而是你第二次、第三次遇到类似变化的时候。
chapter 9:设计模式常见误区
9.1 误区一:为了使用模式而使用模式
设计模式是工具,不是 KPI。
如果一个简单查询接口被拆成了 Factory、Strategy、Context、Holder、Manager、Processor、Invoker,最后只有作者本人能看懂,那不是设计,那是迷宫装修。
好设计应该让复杂问题变简单,而不是让简单问题变复杂。
9.2 误区二:以为模式越多越高级
一个成熟系统不一定到处都是模式名,但它一定有清晰的边界、稳定的抽象和可控的变化点。
高级代码不是“看起来很设计模式”,而是:
- 改需求时影响范围小;
- 新增功能时不用到处改;
- 出问题时能快速定位;
- 新人接手时能理解;
- 业务复杂度没有泄漏到所有地方。
9.3 误区三:只学 UML,不看业务
设计模式必须结合业务场景。
同样是支付系统,有的地方适合策略,有的地方适合责任链,有的地方适合状态机,有的地方只需要一个普通 Service。
不要一看到 if-else 就马上策略模式,也不要一看到流程就马上模板方法。先问一句:这个地方未来会怎么变?
9.4 误区四:忽略团队理解成本
设计模式会引入间接层。间接层能带来扩展性,也会带来理解成本。
如果团队成员都不熟悉某个模式,而业务复杂度又不高,强行使用可能会降低效率。
工程设计永远是权衡。没有免费的抽象,每多一层设计,都应该有它存在的理由。
chapter 10:推荐学习路径
如果你是 Java 后端开发,可以按这个顺序学习设计模式:
第一阶段:先掌握最常用的模式
优先学习这些:
- 策略模式;
- 工厂模式;
- 模板方法模式;
- 代理模式;
- 责任链模式;
- 观察者模式;
- 建造者模式;
- 适配器模式;
- 外观模式;
- 状态模式。
这些模式在 Spring、业务系统、微服务开发中出现频率很高。
第二阶段:结合 Spring 源码理解
可以重点看:
- Spring Bean 创建流程中的工厂模式;
- Spring AOP 中的代理模式;
- Spring MVC 中的适配器模式;
- JdbcTemplate 中的模板方法;
- ApplicationEvent 中的观察者模式;
- FilterChain 中的责任链模式。
不要一上来硬啃全部源码,容易从入门到睡着。建议围绕一个问题去看源码,例如:“Spring AOP 为什么能在方法前后加逻辑?”
第三阶段:在业务代码中重构
学习设计模式最好的方式不是背定义,而是重构坏味道代码。
可以从这些地方开始:
- 把大量
if-else重构成策略模式; - 把复杂创建逻辑重构成工厂或 Builder;
- 把复杂流程拆成模板方法和责任链;
- 把第三方接口封装成适配器;
- 把复杂聚合调用收敛成外观服务;
- 把事件副作用拆成观察者或领域事件。
每次重构时都要问:这次抽象是否真的降低了未来修改成本?
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides,《Design Patterns: Elements of Reusable Object-Oriented Software》。
- Robert C. Martin,《Clean Architecture》。
- Robert C. Martin,《Agile Software Development, Principles, Patterns, and Practices》。
- Joshua Bloch,《Effective Java》。
- Martin Fowler,《Refactoring: Improving the Design of Existing Code》。
- Craig Walls,《Spring in Action》。
- Spring Framework Reference Documentation。
- Refactoring.Guru Design Patterns。
- Oracle Java Documentation。
- GoF 设计模式相关经典资料与 Java 社区实践总结。
启示录
设计模式最重要的不是记住名字,而是识别变化。
当你看到一段代码频繁修改、分支越来越多、类越来越胖、依赖越来越乱时,设计模式才真正有意义。
六大原则是设计模式的底层逻辑:
- 单一职责原则让职责更清楚;
- 开闭原则让扩展更容易;
- 里氏替换原则让继承更安全;
- 接口隔离原则让依赖更轻;
- 依赖倒置原则让业务更稳定;
- 迪米特法则让对象边界更清晰。
最终,好的设计不是为了炫技,而是为了让系统在变化中仍然保持秩序。
写代码像带兵,不能每次敌人换个方向,就把阵型全拆了。真正厉害的设计,是阵型能变,但军心不乱。
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。