设计模式:动机、取舍与语言相关性
设计模式
本文是设计模式的个人梳理笔记,不追求面面俱到,而是聚焦于动机(为什么)与取舍(何时不用)。适合有一定 Java/Spring 开发经验、想建立设计模式全局观的读者。文中"出现概率"均基于笔者在开发过程中的主观感受,仅供参考。
logged in 2020 and written with Claude.
设计模式是为了封装变化,让各个模块可以独立变化。多关注问题本身以及动机。
总纲
- SOLID
- 介绍方式:What(这个模式做什么),Why(为什么选这个),How(怎么用这个模式),Related(与其他模式的关联)
- 设计模式的争议
- Abuse:只有一把铁锤,任何东西看上去都像钉子。
- 往往设计模式是在常规思路、编程语言上没有很好的解决方案时需要采用的。
- Are Design Patterns Missing Language Features
- 往往是 Java 的语言约束导致了这些设计模式的诞生,例如:Java 不支持动态分派(访问者模式)。
- 如果语言支持类型能够当参数进行传递(或动态分派),创建型、Visitor 模式基本失去了意义。
- 如果语言支持函数作为参数传递(或函数为一等公民),行为型基本失去了意义。
- 大部分结构型的方案基本就是将继承转换为组合。
- 因此,在 Kotlin/Scala/Go 中,很多行为型模式可以直接用高阶函数或接口组合替代;本文的模式讨论以 Java/Spring 生态为主要背景。
各模式的语言相关性一览:
分为两类:「语言催生」的模式——本质上是对语言表达力不足的补丁,换一门语言可以天然消除;「问题催生」的模式——解决的是系统复杂度本身,任何语言都需要。
| 类型 | 模式 | Java 中为何需要 | 可天然替代的语言 / 特性 |
|---|---|---|---|
| 语言催生 | 策略 | 无一等函数,必须定义接口+实现类 | Kotlin / Python / JS:直接传 lambda |
| 语言催生 | 命令 | 同上,参数化"动作"只能靠对象包装 | 同上 |
| 语言催生 | 模板方法 | 无高阶函数,只能靠继承参数化步骤 | Kotlin:高阶函数直接作为参数传入 |
| 语言催生 | 工厂方法 / 抽象工厂 | 类不能作为值传递,绕过编译期类型约束 | Python / Ruby:类本身即对象,可直接传递 |
| 语言催生 | 访问者 | 只支持单分派,双分派需手动模拟 | Julia / Common Lisp:原生多分派,模式不再必要 |
| 语言催生 | 装饰 | 无委托语法,每个方法都要手写转发 | Kotlin:by 委托一行搞定;Python:@decorator |
| 语言催生 | 建造者 | 无命名参数 / 默认参数,构造器难以表达可选项 | Kotlin / Python:命名参数+默认值直接解决 |
| 语言催生 | 单例 | 需要手动控制实例化和线程安全 | Python 模块天然单例;Go:sync.Once 惯用法 |
| 语言催生 | 原型 | Cloneable 设计缺陷,深拷贝需手动实现 |
Python:copy.deepcopy;JS:结构化克隆 |
| 问题催生 | 观察者 / 责任链 / 中介者 | 解决通信拓扑复杂度 | 无语言可天然消除,只是写法(channel、signal)不同 |
| 问题催生 | 状态 / 组合 | 解决领域建模中的状态爆炸 / 递归树结构问题 | 同上 |
| 问题催生 | 享元 / 代理 | 解决资源复用 / 访问控制 | 同上 |
| 问题催生 | 适配器 / 桥接 / 外观 | 解决接口兼容、多维变化、子系统隔离 | 同上,换语言只是少写几行,问题本身不消失 |
3. 设计模式可以说是为了弥补编程模型上的一些缺陷,并且各种设计模式往往比较适合局部、偏底层的地方。
- 减少继承
创建型(对象的生成)
1. 单例模式
- 意图:全局只有一个实例对象。
- 动机:仅需要一个单一的实例对象就足够。
- 出现概率:99%
- Pros:管理方便容易。
- Cons:多线程模式下改变单例对象,需要谨慎。
- ✅ 适用场景:线程池、配置中心、连接池等全局唯一资源。
- ⛔ 反模式 / 误用:把业务对象做成单例(如把 Service 的有状态字段放在单例里),多线程下出现数据竞争;测试时难以 mock,降低可测性。
2. 建造者模式
- 意图:使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。
- 动机:减少过多的构造器实现 / 不同的参数,创建不同的对象。
- 出现概率:90%
- Examples:
Executors.newFixedThreadPool()/ThreadPoolExecutor.Builder - ✅ 适用场景:参数超过 4 个、有可选参数的复杂对象构建,如
ThreadPoolExecutor、HTTP 请求体。 - ⛔ 反模式 / 误用:对只有 1~2 个参数的简单对象引入 Builder,徒增模板代码;Builder 内部没有做参数校验,把错误延迟到
build()后才暴露。
3. 工厂方法(Factory Method)
- 意图:在父类中提供一个创建对象的接口,允许子类决定实例化对象的类型。
- 动机:容易扩展创建产品的方式。
- Pros:解耦创建者和具体产品之间的关联。
- Cons:引入了更多的子类。
- ✅ 适用场景:框架需要允许扩展点,让调用方决定创建哪种实现,如日志框架
LoggerFactory。 - ⛔ 反模式 / 误用:每新增一种产品就要新增一个子类,产品类型稳定时用普通静态工厂即可,过度引入子类层级。
4. 抽象工厂模式
- 意图:一个接口提供一套完整的产品,且不需要知道产品的具体种类。
- 出现概率:10%
- Cons:每次新增一个产品时,所有子类都需要进行实现。
- Examples:Spring 中的
BeanFactory - ✅ 适用场景:需要同时切换一整套相关产品族(如跨数据库方言、跨平台 UI 组件)。
- ⛔ 反模式 / 误用:产品种类频繁变动时使用——每次新增一种产品,所有工厂实现都要改,违反开闭原则。
5. 原型模式(Prototype)
- 意图:使你能够复制已有对象,而又无需使代码依赖它们所属的类。
- 结构:实现接口方法
clone() - 出现概率:90%
- Pros:方便地复制对象。
- Examples:Java 的序列化
- ✅ 适用场景:对象创建成本高(深层嵌套、IO 初始化),需要频繁产出相似对象。
- ⛔ 反模式 / 误用:浅拷贝当深拷贝用——共享了内部可变引用,导致"克隆体"互相干扰;Java 的
Cloneable接口本身设计存在缺陷,优先考虑拷贝构造函数或序列化方案。
总结
将类的创建管理收拢到比较少的对象中,例如 Spring IoC 其实也是一种创建型的思路。
结构型(对象如何组合)
1. 代理模式(Proxy)
- 意图:用来修改对原对象方法的屏蔽或者增强。(对外暴露的接口保持一致)
- 动机:RPC 框架代理原本的本地方法、AOP 机制切面增强。
- 结构:实现一致接口,内部进行组合。
- 出现概率:99%
- Pros:开闭原则。
- Cons:创建成本、调用成本增加。
- 关联:装饰器、外观模式、适配器
- Examples:Spring AOP 的核心实现。
- ✅ 适用场景:RPC 透明调用、AOP 日志/鉴权/事务切面、懒加载。
- ⛔ 反模式 / 误用:代理层中混入业务逻辑(代理不应该知道业务);动态代理链过长导致栈帧膨胀,排查困难。
2. 适配器(Adapter)
- 意图:使得不兼容的接口之间能够进行合作。
- 动机:当某个对象无法直接访问另一个接口方法时,可以采用适配器进行衔接。
- 出现概率:50%
- Pros:单一职责原则 / 开闭原则。
- Cons:复杂度增加,性能降低。
- 关联:桥接模式往往用在开发初期阶段,适配器模式一般在维护阶段使用,保证重构的可靠性。
- Examples:
Arrays#asList()/Collections#singletonList() - ✅ 适用场景:集成第三方 SDK、遗留接口迁移、单元测试中替换外部依赖。
- ⛔ 反模式 / 误用:在新系统开发初期就引入适配器——说明接口设计本身有问题,应先对齐接口;适配器套适配器,产生多层适配地狱。
3. 桥接(Bridge)
- 意图:一个类有多个维度的变化时,需要让其进行独立变化。
- 动机:降低维护变化的成本,将每个变化隔离在一个具体的实现中。
- 出现概率:10%
- Pros:单一职责原则 / 开闭原则。
- ✅ 适用场景:渲染引擎(形状 × 颜色)、消息发送(渠道 × 格式)等多维度正交扩展。
- ⛔ 反模式 / 误用:只有一个维度变化时强行引入桥接,过度设计;与继承混用,导致类层级仍然爆炸。
4. 组合(Composite)
- 意图:对象按树状结构组织,并且可以独立地访问某个节点。(类比军队管理)
- 动机:文件系统的管理,套娃行为的场景。
- 结构:递归
- 出现概率:20%
- Pros:开闭原则。
- 关联:装饰器模式 / 责任链模式
- ✅ 适用场景:文件系统、组织架构树、菜单/权限树、表达式树。
- ⛔ 反模式 / 误用:叶节点和容器节点行为差异大时强行统一接口,导致叶节点里出现大量空实现或抛异常。
5. 装饰(Decorator)
- 意图:对原始对象进行封装,并提供增强的接口(包括原来的,以及新增的方法)。
- 动机:无需改动原始类,就可以增强其功能。避免使用子类来扩展,因为继承往往是静态的、单一的,容易导致后期维护成本上升。
- 结构:组合
- 出现概率:70%
- Pros:单一职责原则 / 利用组合替代子类继承。
- Cons:初始化的行为比较不 elegant。
- Examples:Java IO 包 /
Collections.unmodifiableXXXX()/ 编码与压缩 - ✅ 适用场景:
InputStream流式增强、缓存层叠加、权限校验叠加。 - ⛔ 反模式 / 误用:装饰链太深、顺序敏感但文档不清晰;与代理混用时职责不明(装饰关注功能增强,代理关注访问控制);debug 时调用栈难以阅读。
6. 外观(Facade)
- 意图:为复杂子系统提供一个简化的统一接口,降低调用方与子系统的耦合。
- 动机:SPI 机制,可以灵活地采用不同的实现方案(日志、数据库等)。
- 出现概率:99%
- Pros:实现与接口独立,容易替换。
- Cons:该接口与所有对象绑定(因此,这种接口往往是公共的)。
- ✅ 适用场景:提供统一的子系统入口(如 SLF4J 屏蔽具体日志实现)、微服务 BFF 层。
- ⛔ 反模式 / 误用:外观类承担了太多协调逻辑,变成"上帝类";接口粒度过粗导致调用方必须拿到不需要的数据。
7. 享元(Flyweight)
- 意图:共享多个对象中共有且相同的状态,降低内存消耗。
- 动机:同时存在大量的对象,且存在较多的相同的状态。
- 关联:很容易想到单例模式,但是单例模式是享元模式的一种特例。在享元模式中,尽管存在相同的状态,但是仍有不同的一些状态需要独立维护。
- ✅ 适用场景:字符串常量池、
Integer缓存(-128~127)、棋子/粒子等大量相似对象。 - ⛔ 反模式 / 误用:外部状态被误放入享元对象(享元只能保存内部状态,线程安全性会被破坏);过早优化——对象数量并不大时引入享元徒增复杂度。
对比讨论
- 适配器 vs 代理 vs 装饰:适配器改变接口;代理不改变接口,但控制访问;装饰不改变接口,但增强功能。
- 组合 vs 桥接:组合解决"整体-部分"的递归树问题,桥接解决"多维度独立变化"的组合爆炸问题。
- 享元 vs 单例:单例是享元的特例(整个对象共享),享元允许对象携带不同的外部状态。
- 外观:不是对单一对象的包装,而是对整个子系统的统一入口,隐藏内部复杂性。
行为型(对象之间的通信)
1. 责任链(Chain of Responsibility)
- 意图:请求按照一定的处理链进行处理。
- 动机:对不同的请求安排不同的处理链。所有的请求都有固定的处理链。
- 出现概率:50%
- Pros:单一职责原则 / 开闭原则。
- 关联:命令模式 / 中介者模式 / 观察者模式
- Examples:Dubbo Filters / Netty handlers
- ✅ 适用场景:过滤器/拦截器链(Servlet Filter、Dubbo Filter)、审批流、风控规则引擎。
- ⛔ 反模式 / 误用:链条过长且没有短路机制,所有处理器都要执行;处理器之间隐式依赖顺序却没有文档说明,维护时容易踩雷。
2. 命令(Command)
- 意图:将不同的请求转换到统一的命令对象,交给执行器进行执行。
- 动机:将请求与具体的执行者进行解耦,实现参数化操作。
- 出现概率:20%
- ✅ 适用场景:支持撤销/重做(编辑器)、任务队列、宏录制。
- ⛔ 反模式 / 误用:业务逻辑简单时引入命令对象——一个
execute()方法包一层,纯粹增加间接层;命令对象持有过多上下文,退化成贫血模型。
3. 迭代器(Iterator)
- 意图:对集合类的结构能够采用一种统一的方式进行遍历访问。
- 动机:隐藏具体的迭代过程。
- 出现概率:99%
- ✅ 适用场景:统一遍历各种集合/树/图结构,隐藏底层实现。
- ⛔ 反模式 / 误用:在已有 Stream API 的场景下手写迭代器;在迭代过程中修改集合(
ConcurrentModificationException)。
4. 中介者(Mediator)
- 意图:由中介者来协调不同对象之间的合作。
- 动机:对象之间的依赖过于复杂。
- 出现概率:60%
- Cons:中介者变为上帝对象。
- Examples:
Executor#execute()/Timer#scheduleXXX()/Method#invoke()/ 连接池 / YARN 资源管理器 - ✅ 适用场景:聊天室、航空调度、GUI 组件协调(表单联动)、连接池。
- ⛔ 反模式 / 误用:中介者承担过多职责变成上帝对象;系统中只有少量对象时引入,增加间接层而没有实质收益。
5. 备忘录(Memento)
- 意图:使得数据的状态得以快速保存和恢复。
- 出现概率:5%
- ✅ 适用场景:游戏存档、编辑器撤销、事务回滚的状态保存。
- ⛔ 反模式 / 误用:状态对象过大时频繁快照,内存暴涨;没有控制快照数量上限,历史记录无限增长。
6. 观察者(Observer / Listener)
- 意图:实现一种订阅机制,使得被观察对象发生事件时通知到订阅者。
- 动机:有不同的对象要观察监听同一个对象的事件 / 观察者可能动态加入、离开观察者队列。
- 出现概率:80%
- Pros:开闭原则。
- Cons:无法控制通知的顺序。
- Examples:DataStream / RxJava
- ✅ 适用场景:事件驱动、消息总线、MVC 中 Model 通知 View。
- ⛔ 反模式 / 误用:观察者链式触发(A 通知 B,B 又通知 C),调试极其困难;观察者持有被观察者的强引用,忘记取消订阅导致内存泄漏;同步通知中观察者执行耗时操作,阻塞主流程。
7. 状态(State)
- 意图:使得对象在改变自身状态的时候,改变自身的行为。
- 动机:将状态变更与对象的自身逻辑分离。
- 出现概率:70%
- Pros:单一职责原则 / 开闭原则 / 解耦。
- Cons:在状态较多或变更条件复杂的时候才适合采用。
- Examples:业务对象的
draft → submitted → effective → to be offline → offline状态变化,简化整体业务代码。 - ✅ 适用场景:订单状态机、工作流引擎、TCP 连接状态、游戏角色状态。
- ⛔ 反模式 / 误用:状态数量只有 2~3 个时引入,if-else 更直观;状态之间的转移逻辑散落在各个 State 类里,全局迁移图难以一眼看清(建议配状态机图)。
8. 策略(Strategy)
- 意图:能够根据上下文情况,方便切换具体的实现算法。
- 动机:有较多的实现方式,并且希望能够在运行时动态切换。
- 出现概率:50%
- Pros:实现与流程隔离。
- Cons:Client 需要自己清楚什么时候采用什么策略。
- ✅ 适用场景:排序算法切换、支付方式选择、促销规则计算、路由策略。
- ⛔ 反模式 / 误用:策略类激增但大多数场景只用 1 种策略——可用函数式接口(
Function/Predicate)替代;Context 里残留大量 if-else 选策略的逻辑,没有真正解耦。
9. 模板方法(Template Method)
- 意图:在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。
- 动机:多个类的执行过程,只有在局部有一些变化,可以采用模板方法进行抽象。
- 结构:继承
- 出现概率:80%
- Pros:减少了大量 duplicate code。
- Cons:必须在编译时就要能够明确具体是谁执行。
- 关联:策略模式是在运行时进行切换,而模板方法很明显是在编译时就决定了。
- ✅ 适用场景:数据处理框架(读取 → 处理 → 写出)、JUnit 的
setUp/tearDown、AbstractList。 - ⛔ 反模式 / 误用:子类重写了不应该重写的步骤(“脆弱的基类"问题);钩子方法过多,子类需要理解整个父类流程才能正确实现;在需要运行时切换算法时误用模板方法(该用策略)。
10. 访问者(Visitor)
- 意图:将算法与访问的对象隔离。
- 动机:同一组对象会有不同的方式被访问(aka. 基于其进行计算)。
- 出现概率:10%
- Cons:访问者是强关联到这个数据结构上的。
- Examples:Druid SQL 树的计算
- ✅ 适用场景:编译器 AST 遍历、SQL 语法树分析(如 Druid)、文档格式导出。
- ⛔ 反模式 / 误用:数据结构频繁变动时使用——每加一种元素类型,所有 Visitor 都要改;访问者强依赖数据结构的
accept方法,破坏了封装性。
总结
设计模式本身并不重要,而在于它采用了哪些策略来降低系统复杂度、提高系统的可维护性。
- 减少继承,多采用组合的方式来丰富类的功能。
- 面对各种功能的变更,能够将变化(影响)的范围尽可能的缩小到 1 个或者少量的类中。
- 面向接口编程。
何时应该克制:设计模式的引入本身也是一种成本——间接层增加、代码量上升、新人理解曲线变陡。在以下情形应克制使用:
- 团队规模小、迭代快,过度抽象反而拖慢速度;
- 变化点还不明确,过早抽象容易抽错方向(YAGNI 原则);
- 语言/框架已经原生支持,例如 Java 8+ 的函数式接口基本覆盖策略模式的使用场景。
参考
- https://refactoringguru.cn/design-patterns/prototype
- https://design-patterns.readthedocs.io/zh_CN/latest/index.html
- https://www.cnblogs.com/zuoxiaolong/p/pattern26.html
- https://www.zhihu.com/question/23757906
- Are Design Patterns Missing Language Features
- 往往是 Java 的语言约束导致了这些设计模式的诞生
最后修改于 2020-03-30