ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java设计模式实战:从面向对象到23种模式的核心解析

Java设计模式实战:从面向对象到23种模式的核心解析

1. 先别急着背模式,搞清楚它们到底在解决什么问题

1.1 设计模式不是什么高深的东西

我第一次接触设计模式的时候,是在某次项目重构的会议上。那时候一个订单模块的代码堆了两千多行,一个类里既有创建订单的逻辑、又有折扣计算的逻辑、还有状态流转的判断,改一个需求要小心翼翼翻半天代码。当时的某位前辈看完之后说了一句话:"你这里应该用策略模式,那边用个状态模式就清爽多了。"

说实话,当时我是懵的。策略模式?状态模式?这些名字听着挺唬人,但仔细想想,它们其实就是在说一件事:把变化的部分从不变的部分里剥离出来。

设计模式不是某个天才发明出来的空中楼阁,它是前辈们在无数个项目里踩坑踩出来的通用解法。你可以把它理解为程序员界的"菜谱":红烧肉有很多种做法,但万变不离其宗的步骤就是焯水、炒糖色、慢炖。设计模式做的就是这个事——把一类常见问题的解决步骤归纳成可复用的套路。

Java 语言里的 23 种设计模式,最早是四个人(GoF,Gang of Four)在《Design Patterns》这本书里总结出来的。书里把它分成三大类:创建型、结构型、行为型。这个分类不是随便分的,它对应着你在设计一个系统时最关心的三件事:对象怎么创建、类和对象怎么组合、对象之间怎么交互。

很多人学设计模式的时候,上来就拿着 UML 类图去死磕,什么依赖倒置、接口隔离,背了一堆概念,结果到项目里还是写不出来。问题出在哪?出在把"模式"当成了"知识"而不是"工具"。知识是可以背的,工具是用会的。所以这篇文章我不会铺开 23 种模式逐个念定义,而是把每一种模式背后解决的核心问题、使用场景、容易踩的坑讲明白,让你看完之后至少知道:项目里遇到这个问题,我该用哪个套路,为什么。

1.2 为什么是 23 种,这个数字怎么来的

先纠正一个常见的误解:23 种设计模式不是 Java 独有的,它跟语言无关。GoF 在 1994 年出版的那本书里,从大量实际的面向对象系统中提炼出了这 23 种通用模式。之所以叫"23 种",是因为当时作者们归纳来归纳去,发现这几个套路能覆盖绝大多数反复出现的设计问题。

这个数字本身不重要,重要的是它的分类逻辑——三大类:

  • 创建型模式(5 种):单例、工厂方法、抽象工厂、建造者、原型。解决的核心问题都是"怎么让对象的创建更灵活、更可控"。创建型就是管理对象的诞生过程,别让你的代码到处 new。
  • 结构型模式(7 种):适配器、装饰器、代理、外观、桥接、组合、享元。解决的核心问题是"怎么把类和对象组织成更大的结构,同时保持灵活"。
  • 行为型模式(11 种):策略、模板方法、观察者、迭代器、责任链、命令、备忘录、状态、访问者、中介者、解释器。解决的核心问题是"对象之间怎么分配职责、怎么通信"。

你如果仔细看会发现,这三大类其实对应的是你写代码时的三个时间节点:创建对象时、组织对象时、对象运行时。一个系统从设计到运行,逃不开这三件事。所以学设计模式,先理解这个分类框架,比一个个念名字重要得多。

1.3 掌握设计模式的前提:面向对象的基本功

这句话可能有点扎心,但我还是得说:设计模式是建立在面向对象三大特性之上的,如果你对封装、继承、多态理解不到位,学设计模式会非常痛苦。

举个例子,策略模式看起来很简单,就是定义一个接口、多个实现类、然后在运行时选择具体实现。但你要是没吃透"多态",你很难理解为什么明明可以 if-else 判断,非要搞出三五个类来。多态的本质是"调用方不关心具体实现,只关心接口约定",策略模式把它放大到了算法层面。

再比如装饰器模式,它的核心是"组合优于继承"。Java 里的 IO 流就是典型例子:new BufferedInputStream(new FileInputStream(...))一层套一层,靠组合给对象动态增加功能。你如果不理解"组合"和"继承"的取舍逻辑,看到这种嵌套的写法会觉得莫名其妙。

所以如果你觉得设计模式学起来像天书,先回头补一下面向对象基础:接口和抽象类的区别、多态在运行时怎么起作用、组合和继承各自适合什么场景。基础打牢了,设计模式其实就是一层窗户纸。

2. 创建型模式:对象怎么来,往往比对象本身更重要

2.1 单例模式:一个类的"唯一身份证"和它的三个坑

单例模式应该是 23 种模式里最简单、也是最容易被用烂的一个。它的核心诉求只有一句话:某个类在整个系统里只能有一个实例。典型的场景是配置管理器、线程池、数据库连接池、日志对象等,这种全局共享的资源,如果出现多个实例,要么浪费资源,要么状态不一致。

Java 里写单例的方式有很多种,我直接说结论,日常开发用枚举单例或者静态内部类单例最稳妥。静态内部类写法是这样的:

public class ConfigManager { private ConfigManager() {} private static class Holder { private static final ConfigManager INSTANCE = new ConfigManager(); } public static ConfigManager getInstance() { return Holder.INSTANCE; } }

这种方式既做到了懒加载(第一次调用时才创建),又利用了 JVM 的类加载机制保证了线程安全,还避免了synchronized锁带来的性能开销。

单例模式看起来简单,但实际用起来坑不少。我踩过最典型的三个坑:

第一,在多线程环境下用双重检查锁(DCL)时,忘了给实例字段加 volatile。Java 指令重排在极端情况下会让其他线程拿到一个"半初始化"的对象,这种 bug 很难查,因为它不是每次都出现。

第二,把单例当成全局变量用。有些同学发现单例方便,就什么数据都往里面塞,结果单例类变成了一个巨大的状态容器,代码耦合度急剧上升。单例的本质是"控制实例数量",不是"提供一个全局存储区"。

第三,序列化和反射会破坏单例。如果你的单例类实现了Serializable,反序列化时会重新创建实例;反射调用私有构造器也能开辟新实例。当然,绝大多数业务系统不会遇到这种极端情况,但如果你的代码是给框架用的,需要留意这个问题。

我个人的态度是:单例该用就用,但别滥用。一个系统里用到十个以上的单例,你就要反思是不是设计有问题——很多情况下,用依赖注入容器(比如 Spring 的@Component,它默认也是单例的)可以更好地管理对象生命周期,没必要自己手写单例。

2.2 工厂方法 vs 抽象工厂:别再把它们混为一谈

工厂模式可能是面试里被问得最多的创建型模式了,但也是被误解得最深的。很多人觉得工厂模式就是"把 new 的过程封装到一个类里",这种理解太皮毛了。

先说工厂方法模式。它解决的核心问题是:当一段代码需要创建某种对象,但又不确定具体创建哪一个子类时,把"创建对象的决定权"交给子类。简单来说,父类定义了一个"创建对象的接口",子类决定实例化谁。

想象一个日志框架,现在有很多种日志:文件日志、控制台日志、数据库日志。你希望框架本身不关心日志类型,只负责调用"创建日志器"的方法,而具体的创建逻辑由配置文件决定。这就是工厂方法模式的典型场景。

public abstract class LoggerFactory { public abstract Logger createLogger(); public void writeLog(String message) { Logger logger = createLogger(); logger.log(message); } }

再来说抽象工厂模式。它的核心场景是:创建一组相互关联的对象,而且这组对象可能有多种"产品族"。举个好理解的例子,UI 组件库里有按钮、输入框、弹窗,而不同操作系统的控件风格不一样:Windows 风格的按钮和 Linux 风格的按钮观感不同。抽象工厂就是提供一个接口,让子类生产"配套的一整套产品"。

  • 如果你只需要创建一种产品,用工厂方法。
  • 如果你需要创建一组配套的产品,而且它们之间存在约束关系(比如 Windows 的按钮必须配 Windows 样式的弹窗),用抽象工厂。

这两个模式最大的区别不是代码形态,而是服务对象的粒度。工厂方法服务于"一个产品对象",抽象工厂服务于"一组产品族"。很多同学把一堆工厂类写在同一个包里,命名都是XxxFactory,看起来差不多,实际职责完全不同。你写代码之前先想清楚:我是要选择一个产品,还是要选择一条产品线。

2.3 建造者模式与原型模式:搭积木和抄作业

建造者模式解决的核心问题是:一个对象的属性太多,构造器参数列表长得不忍直视,而且部分参数可选。遇到这种类,最简陋的做法是写一堆重载构造器,或者开一堆 setter。重载构造器的问题是调用方搞不清楚每个参数的含义,setter 的问题是这个对象的中间状态可以被外部修改,不可保证完整性。

我见过最典型的一个例子是某个用户实体类,有十几个字段,包括姓名、年龄、邮箱、手机号、地址、爱好集合等等。早期代码里就是new User("张三", 25, "xxx@xx.com", "138...", "上海", ...),看完这一行你根本不知道第三个参数是邮箱还是地址。后来改造成建造者模式后,代码变成:

User user = User.builder() .name("张三") .age(25) .email("xxx@xx.com") .city("上海") .build();

每个参数都带着名字,可读性直接提升一个档次。Java 领域里很流行的 Lombok 注解@Builder就是为此设计的。

原型模式则完全是另一个思路:它不是通过 new 来创建对象,而是复制一个已有的对象作为模板。它的核心价值在于:当“创建对象的过程非常昂贵”(比如需要查数据库、做大量初始计算)时,直接复制已有对象能大幅提升性能;同时,它让对象创建不依赖具体的类。

Java 里实现原型模式最直接的途径是Cloneable接口,但这里有个很大的坑:Object.clone()是浅拷贝,如果你的类里有引用类型的成员变量,复制出来的对象和原对象会共享同一个子对象。一旦你改了副本的嵌套对象,原对象也跟着变了,这是很多隐蔽 bug 的来源。所以用原型模式时,必须明确你需要深拷贝还是浅拷贝,深拷贝通常需要自己重写clone()方法,或者用序列化、手写拷贝构造器。

我个人的看法是:现在 JVM 创建对象的成本已经非常低了,如果不是高频创建的场景(比如每秒几十万次的缓存对象生成),原型模式用到的机会并不多。但理解它的存在,能让你在看到"复制对象"这类需求时,多一种设计思路。

3. 结构型模式:把类和对象搭出好看的骨架

3.1 适配器、装饰器、代理:一字之差,用途天壤之别

结构型模式里,适配器、装饰器、代理是三组非常容易混淆的模式,因为它们代码形态上都是"包一层"。但它们的设计意图完全不同,这是深入理解设计模式很关键的一点。

先说适配器模式。它的作用是:让两个不兼容的接口能够协同工作。比如你有一个老系统里的XmlOrderService,只提供 XML 数据。新系统里其他模块都需要 JSON 格式。你不可能重写老系统,所以写一个适配器,把 XML 转成 JSON 再对接新模块。适配器解决的是"接口不匹配"的问题,核心在于转换。

然后是装饰器模式。它的作用是:在不修改原有类的情况下,动态地给对象添加新功能。最经典的例子就是 Java IO,new BufferedReader(new InputStreamReader(new FileInputStream(file)))一层层包裹,每一层都增加了新的能力。装饰器解决的是"功能扩展"的问题,核心在于增量叠加。

最后是代理模式。它的作用是:控制对对象的访问,在访问前或者访问后执行额外的逻辑。分类上又分静态代理和动态代理。Spring AOP 的底层原理就是动态代理。代理和装饰器在代码结构上非常像,很多人分不清。区别在于:装饰器是"功能增强",代理是"访问控制"。你写了一个日志记录的类包住真实对象,这既可能是装饰器也可能是代理,关键看你的意图。如果你是为了在调用前后记录日志,这是代理;如果你是为了给对象本身新增功能,这是装饰器。

这三者放在一起对比一下更直观:

模式解决的核心问题典型场景关键特征
适配器接口不兼容新旧系统对接转换接口
装饰器动态增加功能Java IO 流层层包裹、功能叠加
代理控制访问权限校验、日志、延迟加载访问前后附加逻辑

碰到"包一层"的代码,别急着认定是哪种模式,先想清楚背后的意图。

3.2 组合模式与桥接模式:树形结构和多维变化

组合模式解决的核心问题是:让"部分"和"整体"能用一致的方式处理。最典型的例子是文件系统:一个文件夹里面可以有多个子文件夹和文件,无论你操作的是一个文件还是整个目录,都用同样的接口。

Java 的HashMap里的putAll方法就是一个类似思想的体现——你传一个Map进去,不管它是小 map 还是嵌套的大 map,处理逻辑一致。组合模式的核心结构是树形的:叶子节点和分支节点都实现同一个接口,分支节点里持有子节点的集合。

实际编码中,组合模式常用在菜单系统、组织架构、分类目录这种天然有层级关系的场景。你需要记住的关键点是:不要在设计模式代码里写出if (node instanceof Leaf)这种判断——那就没体现统一接口的价值了。

桥接模式则解决另一个维度的问题:当一个类有两个维度的变化时,不要用继承去无限膨胀,而是把两个维度拆开,各自独立扩展,再通过组合的方式桥接起来。

我用一个通俗的例子说明。假设你要做一个图形绘制系统,图形的类型有圆形、方形,而画笔的颜色有红色、蓝色。如果你用继承来实现,要建:圆形红笔、圆形蓝笔、方形红笔、方形蓝笔,4 个类。如果再加一个三角形,就变成 6 个类。继承体系会急速膨胀。

桥接模式的做法是:把"图形"和"绘制的颜色"拆成两个独立的类层级,图形里持有颜色的引用,画图形的时候委托颜色去执行绘制。这样以后再增加三角形,只需要加一个图形子类,不再需要关心颜色有多少种。核心思想就是用组合代替继承,破解多维度继承爆炸。

这个模式在实际框架里很常见,比如 JDBC 的驱动体系:JDBC API 是抽象层,具体的数据库驱动是另一层,两者通过DriverManager桥接起来。理解桥接模式,你再看那些"既有多维度扩展、又不想搞一堆子类"的设计时,思路会清晰很多。

3.3 外观模式与享元模式:门面简化调用,细粒度复用

外观模式(Facade Pattern)在我看来是最容易提升代码友好度的模式之一。它解决的核心问题是:子系统内部很复杂,调用方看不懂也不关心,你只要提供一个简单的门面接口,把内部的复杂性全部挡住。

举个例子,你搭建了一个家庭影院系统,包含投影仪、音响、灯光、幕布。如果调用方要看电影,需要依次开启幕布、开投影仪、开音响、调暗灯光、选择片源。每次都得调用一堆方法,调用方直接被细节淹没。有了外观模式,你只需要提供一个MovieFacade.watchMovie()方法,内部替调用方搞定全部流程。

这个思想的本质是"接口简化"。很多开源框架的入口只有一个类,为什么?因为它不想让使用方陷入内部实现的泥潭。分层架构里的 Service 层实际上也扮演了门面的角色:Controller 不关心你的 Service 内部用了几张表、调了几个外部接口,它只需要一个placeOrder()方法。

享元模式则是一个偏底层优化的模式。它解决的核心问题是:大量细粒度的对象被反复创建,造成内存浪费,能不能共享这些对象?经典的场景是字符串常量池、Integer 缓存(-128 到 127)、线程池。

享元模式的关键是把对象的状态分成两类:内部状态(可以共享的,比如字符本身)和外部状态(不能共享的,比如字符的位置)。代码层面,享元通常配合工厂来使用:工厂维护一个对象池,请求过来时先查池子,有就直接返回,没有再创建。

日常业务开发里,享元模式用得不多,但在写工具库、底层框架、游戏开发时,它是一个有效的内存优化手段。你只需要记住:当你的系统里出现了大量重复的、不可变的对象时,想想能不能把它们共享起来。

4. 行为型模式:真正考验设计功力的部分

4.1 策略模式与模板方法模式:算法的横向复用和纵向复用

行为型模式里,我觉得对日常开发帮助最大的是策略模式。它解决的核心问题:一段代码中,某个算法有多种实现方式,而且可以互相替换,调用方不希望和具体算法绑定。

最经典的例子是促销折扣:一个订单有多种折扣类型,满减、打折、无优惠。如果你写 if-else,每增加一种折扣,就要改那段判断代码。而这个"判断逻辑"本身就是一种不稳定需求,if-else 会让你的代码不断膨胀,最终变成"彩虹类"(到处都是条件分支)。

策略模式的代码套路很简单:

public interface DiscountStrategy { BigDecimal applyDiscount(BigDecimal amount); } public class FullReductionStrategy implements DiscountStrategy { ... } public class PercentageStrategy implements DiscountStrategy { ... } public class NoDiscountStrategy implements DiscountStrategy { ... }

调用方只需要持有DiscountStrategy接口,在运行时传入具体实现即可。这个模式的精髓在于:它把"选择权"从调用方内部挪到了外部,让算法可以独立扩展。

策略模式在框架源码里无处不在,比如Comparator接口,比如 Spring 里的HandlerMapping。你写业务代码时,如果有多个 if-else 分支是在做同一件事的不同做法,优先考虑用策略模式代替。

模板方法模式和策略模式是互补的。策略模式把算法的不同部分交给外部替换,而模板方法模式则是把一个流程的骨架定死,把其中某些步骤留给子类去实现。

举一个很生活化的例子,泡茶和泡咖啡的流程是相似的:烧水、放原料、冲泡、加调料。如果把"泡饮品"做成一个模板方法,烧水是固定的步骤,放什么原料由子类决定。这就是模板方法模式。

模板方法的核心价值在于防止流程被破坏。如果你用策略模式让调用方自己拼装流程,调用方可能漏掉某一步;而模板方法把流程锁死在父类里,子类只能改细节,不能改骨架。这在很多框架的扩展点里非常常见。

4.2 观察者模式与责任链模式:事件驱动的两种经典范式

观察者模式解决的核心问题:当一个对象的状态发生变化时,希望其他一组对象都得到通知,但它们之间不应该耦合。最典型的就是 GUI 里的按钮点击事件、订阅发布系统、消息通知。

Java 里有个很古老的Observable类和Observer接口,不过谁都会用。更现代的做法是直接自己定义一个简单的监听器接口,或者用框架的事件机制(比如 Spring 的ApplicationEvent)。观察者模式的本质是把"状态变化"作为事件推送给关注者,让发布方不依赖具体的订阅方。

这个模式有个常见误区:观察者之间的关系是广播式的,一个事件发出,所有订阅者都会收到。如果你需要的不是广播,而是有选择地通知且按顺序处理,那就要考虑责任链模式。

责任链模式解决的核心问题:一个请求可能被多个对象处理,而且处理者之间的顺序是动态的,你要把它们串成一条链,请求沿着链传递,直到某个结点处理它为止。最典型的例子是审批流:请假一天和请假五天走的审批人不一样,请假日期越长,审批链越长。还有 Java Web 领域的 Filter 过滤器链也是典型的责任链。

这两个模式的共同点是"事件驱动",但区别很明显:观察者是一对多的广播,责任链是一对一的接力传递,每个处理者都有机会处理,也可以选择不处理,交给下家。

4.3 其余行为型模式:用一张表和几个例子串起来

除了策略、模板方法、观察者、责任链,行为型模式还有 7 种,我结合自己的经验快速过一遍:

迭代器模式:让你不暴露内部结构就能遍历集合元素。Java 的Iterator就是它的实现。日常开发里你大概率不需要自己写迭代器,但理解它能帮你读懂集合源码。

命令模式:把请求封装成对象,从而支持撤销、队列、日志等操作。GUI 里的"撤销"操作就是命令模式的典型场景。我见过一些任务调度框架也在用命令模式,把"要执行的操作"变成可序列化的对象。

备忘录模式:保存对象某个时刻的状态,以便以后恢复。典型的例子是文档编辑器里的"撤销到某个版本"。现在的分布式系统通常不这么做,而是用事件溯源,但备忘录的思维是相通的。

状态模式:对象的行为取决于它的状态,状态改变时行为随之改变。这个模式和策略模式在代码上很像,但意图不同:策略模式是"算法可替换",状态模式是"状态转换导致行为切换"。订单状态流转是它的最佳实践场景。

访问者模式:在不修改对象结构的前提下,给对象增加新的操作。这个模式理解起来稍微抽象,实际业务代码里用得不多,但很多编译器框架、AST 遍历里很常见。如果你不是在做编译类或语义分析类系统,可以先了解它,不用强求自己写出来。

中介者模式:用一个中介对象封装一组对象之间的交互,避免对象之间直接互相引用。聊天室、消息队列、系统之间通过中间件通信都是这个思想。

解释器模式:定义一门语言的文法,用它解释句子。正则表达式引擎、SQL 解析器都是它的忠实拥趸。日常业务开发中自己写解释器的场景极少,但了解它能让你明白解析类系统是怎么运转的。

这 7 种模式我给一个快速索引表:

模式解决的核心问题日常碰到的概率
迭代器遍历集合但不暴露内部结构经常用,但不用自己写
命令把请求封成对象,支持撤销/队列中,任务系统常见
备忘录保存并恢复状态低,但思路重要
状态状态转换驱动行为变化高,订单/流程类系统
访问者不修改结构,增加新操作低,编译/分析类
中介者对象之间不直接互相引用中,中间件思想
解释器解析并执行文法低,框架类系统

5. 把设计模式落到项目里:选型思路与实战示例

5.1 判断"要不要用一个模式"的三个问题

模式用对了是解耦,用错了是过度设计。我自己决策时,一般会问三个问题:

第一个问题:这里真的有变化吗?设计模式是为了应对"变化"而存在的。如果一段代码在一个系统生命周期内都不太可能变化,那用设计模式就是画蛇添足。比如你一个小型工具类,内部就一种算法,未来也不会加第二种,那就老老实实写普通类,没必要搞策略模式。我自己见过最离谱的例子是,一个只有一个实现的PaymentService接口,为了"面向接口编程"硬生生创建了接口和实现两个文件,还配了一个工厂类。这种代码你看着很"规范",实际上是空转。

第二个问题:用了模式之后,代码真的更简单了吗?很多时候,两种方案,一个代码量更少、但扩展性差一点,另一个代码量更多、但扩展性好很多。如果你的系统只有两三个调用场景,我倾向于选择前者。设计模式的本质是"用结构换灵活性",当灵活性没有实际价值时,这个交易不划算。

第三个问题:模式的代价我承担得起吗?任何模式都会增加类数量,增加阅读成本。一个用了大量模式的项目,新人上手会比较痛苦。你需要权衡的是:这个项目的生命周期有多长?会不会频繁变更?团队的整体水平怎样?如果你是在一个三年交付一次的银行核心系统里,那多用点模式没问题;如果你是在一个快速迭代的活动运营系统里,简单直接才更高效。

5.2 一个实战例子:订单状态流转的设计

我们来设计一个订单取消流程。这个场景里,订单有很多状态:待支付、已支付、配送中、已完成、已取消。不同的状态下,用户点击"取消订单",系统执行的行为是不同的:

  • 待支付状态:可以直接取消,不需要退款。
  • 已支付状态:取消后需要走退款流程,退款金额还得算上用的优惠券。
  • 配送中状态:不能直接取消,要进入售后审核流程。

初学阶段的写法,大概率是一个cancel()方法里堆 if-else:

public void cancel(Order order) { if (order.getStatus() == Status.PENDING_PAYMENT) { // 直接置为取消 } else if (order.getStatus() == Status.PAID) { // 退款 } else if (order.getStatus() == Status.SHIPPING) { // 售后审核 } }

这种写法的缺点很明显:每加一个状态,就要改这里的判断;每加一种新动作(比如"取消后发短信"),也要改这里。多个状态、多个动作,最后这方法会膨胀成一个巨大的分支怪物。

用状态模式重新设计:把每种状态封装成一个类,状态的切换和该状态下能做的操作都由这个类自己决定。订单对象持有一个"当前状态"对象,调用order.cancel()时,其实委托给当前状态对象去执行。

这样做的直接好处是:新增一个状态,不用动已有的状态类;调整状态流转关系,也只需要改对应的状态类。这个模式在订单、审批、工单这类状态繁多的系统里,确实能救你一命。

如果状态本身不太复杂,但"取消行为"的算法有多种,那更适合策略模式。比如同一个状态下,普通用户取消、管理员强制取消、超时系统取消,几种取消策略各有各的处理逻辑。这两种模式的界限在代码形态上确实接近,我在实际工作中也会把状态模式和策略模式组合使用:用状态模式管理状态流转,用策略模式处理"每种状态下可执行的多种算法"。

5.3 对照框架源码学,比对着书背快得多

很多人学完设计模式,最大的困惑是:我知道每个模式长什么样,就是不知道实际项目里它长什么样。一个很有效的方法是,拿你日常用的框架源码当"活教材"。

比如 Spring 里的BeanFactory,它本质上就是一个抽象工厂;Spring AOP 的动态代理,就是代理模式的教科书;Spring MVC 里的HandlerMapping,是策略模式;ApplicationContext容器启动过程,是模板方法模式;事件发布机制,是观察者模式;Filter链和拦截器链,是责任链模式。

你打开 JDK 源码也是一堆宝藏:Collections.sort()依赖Comparable/Comparator,这是策略模式;HashMap的迭代器,是迭代器模式;Integer.valueOf()里的缓存,是享元模式;ByteArrayInputStream和BufferedInputStream的组合,是装饰器模式。

在框架层面你见到的不是一个个孤立的名词,而是一套被验证过的真实用法。我的建议是,不是先学完 23 种再去读源码,而是遇到一个模式,就去源码里找它的实例。这样你会发现,原来模式不是设计稿里那几行类图,而是活的代码。

6. 学习路径、常见误区和面试避坑

6.1 不要背模式,要背"问题"

我面试过不少候选人,设计模式这块的差距非常明显。有些人能一字不差地背诵某个模式的定义,但问"你这个项目里的权限模块可以用什么模式,为什么",他就答不上来。有些人可能连模式名字都说不全,但会跟你说"我在这里搞了个策略,因为以后鉴权方式会有多种",这种人我是愿意给高分的。

学设计模式的正确姿势,是倒过来学:先看问题,再看解法。比如你看到了一个"多个处理者依次尝试处理请求"的流程,你意识到"这是一个链条",然后才知道有个名字叫责任链模式。如果你先背了责任链的概念,再去找场景,会显得很生硬。

我自己学习的时候,很喜欢用"意图对比"的方式。比如上面提到的适配器、装饰器、代理,它们代码结构都很像,但有各自的意图。我会画一张表把它们的意图列出来,反复对比。这是理解设计模式最扎实的方法:搞清楚这个模式为什么存在,它想解决什么矛盾,用了什么代价换取什么收益。

6.2 常见错误的使用场景

我在代码评审时,见过不少"为用模式而用模式"的情况。这里帮大家排一排雷:

第一个雷:给一成不变的流程硬套模板方法。有些同学把一个只有唯一实现的流程拆成抽象方法挂到父类里,子类只有一两个,还要重写抽象方法。这不是模板方法,这是给代码搭架子。模板方法适用的是"骨架稳定、部分步骤需要被子类定制"的场景,如果根本没有多个子类,就别为了模式而抽象。

第二个雷:滥用单例导致全局状态混乱。有些代码把可变的状态放进单例里,两个线程同时改同一个字段,没有任何同步措施。这就是设计原则没搞对,单例应该倾向于保存无状态的服务类,而不是有状态的数据类。

第三个雷:过度使用装饰器,让调用方看不懂对象被包了多少层。装饰器虽然灵活,但每一层的对象都是不同包装,调试时输出会很痛苦。我见过一个写的异常复杂的日志方案,一个Logger被先后包装了格式化、过滤、缓冲、路由四层,新人看到那几行嵌套构造器直接懵了。装饰器可以用,但要控制层数,或者在工厂里封装好,让调用方只通过工厂获取最终对象。

第四个雷:在不该用观察者的地方硬用观察者,导致"幽灵事件"难以追踪。事件机制虽然解耦,但也带来了隐式控制流——代码里看不到直接的调用关系,出 bug 时很难追踪。如果你的系统里事件发出去没有明确消费者,或者消费者很多且不可控,建议先想想是不是直接调用更清晰。解耦不是目的,可维护才是目的。

6.3 推荐的学习路径和实操方法

如果你是想系统学透设计模式,我建议按下面的路径走:

第一阶段:建立分类框架。先记住 23 种模式分哪三类,每类解决什么问题。不用深入每个细节,目标是看到模式名字时,能说出它属于哪类、核心意图是什么。

第二阶段:逐个精读核心模式。挑选场景最常用的模式,至少精读这几个:单例、工厂(含抽象工厂)、建造者、代理、装饰器、适配器、策略、观察者、模板方法、状态、责任链。每个模式都配套做一个小 demo,用代码验证你的理解,并对照源码找实例。

第三阶段:回看自己的项目代码。把你之前写的业务代码里,if-else 密集的函数、类爆炸的地方标记出来,试着用学过的一两个模式重构一下。注意重点不是"重构完代码变好了",而是"我能不能识别出这里适用什么模式"。这个识别能力,才是面试和真实开发中最稀缺的。

第四阶段:读框架源码。Spring、MyBatis、Netty,随便挑一个你常用的框架,从启动入口开始顺着读,遇到熟悉的模式就停下来标注。这一步能帮你把"模式"从纸面上的类图,变成现实中可触摸的代码。

有一些朋友会问我,要不要学 UML 类图。我的建议是:类图不用画得多漂亮,但一定要会看。模式和 UML 的核心关系是,模式文档通常用类图表达静态结构,用时序图表达动态交互。你在理解模式时,脑子里至少要能想象出"谁持有了谁、谁调用了谁"。我会建议你用简单的类图符来梳理:一个方框是一个类,箭头代表依赖或继承,先会画这个就够用了,不用纠结那些花哨的符号。

7. 最后分享一点我的个人体会

从我自己的经历来说,真正搞懂设计模式,标志不是"我能默写 23 个模式"或者"面试时能说出类图的每个符号",而是你在写代码的时候,脑子里自动浮现出一种"气味判断"——看到一堆 if-else 时你意识到这里有变化点,看到两个不匹配的接口对接时你想到适配器,看到一个对象需要被多个逻辑同时观察时你想到事件机制。

这种能力不是看书看出来的,一定是靠调试、踩坑、重构积累出来的。我学设计模式的时候也走过弯路:有一段时间恨不得把每个类都套个接口,每个业务流程都拆成策略+模板+观察者,结果代码复杂度飙升,同事看我的代码都想骂人。后来才慢慢明白,设计模式的核心是应对确实存在的变化,不是让代码看起来"高大上"。

所以你读完这篇文章,我不希望你去背模式清单,而是希望你记住三个词:意图、变化、权衡。每个模式都是一种权衡交易,没有银弹。等你把三大类、23 种模式的意图搞清楚,并且在真实代码里验证过它们的使用场景后,你会发现自己看代码的视角完全不一样了——你看到的不再是一行行语句,而是一个个结构,以及它们之间的通信方式。

这不是一遍就能吃透的事,但值得花时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进