设计模式全解:23种核心模式的一文读懂

设计模式全解:23种核心模式的一文读懂

前言写代码这件事,写得多了,你会发现自己总是在重复遇到相似的结构问题:

系统需要某个对象只存在一份,你写了一个全局变量——结果测试的时候发现根本没法mock。

一个接口有多种实现方式,你写了一大堆if-else——结果每次加新实现都要改老代码。

对象创建过程太复杂,你把构造函数写得老长——结果调用方根本不知道该传什么参数。

这些问题,前人早就遇到过、踩过坑、总结过解法——这些解法,就是设计模式。

1994年,四位软件工程领域的顶尖专家——Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides(人称GoF,即 Gang of Four,四人组),出版了一本划时代的书《Design Patterns: Elements of Reusable Object-Oriented Software》,系统性地归纳了23种设计模式。这本书成了软件工程领域的基石,"设计模式"这个词也从那个时候开始进入每个程序员的视野。

23年过去了,这23种模式依然是面向对象设计领域最核心的知识体系。不管你用Java、Python、Go还是TypeScript,这些模式的思路都是通用的。学会了设计模式,你看源码的时候会更容易理解"为什么要这么写",写新代码的时候也会有更多的选择。

23种模式一图总览先上一个全局图,建立整体印象:

分类模式名称核心目的创建型单例模式确保只有一个实例创建型工厂方法模式子类决定创建什么对象创建型抽象工厂模式创建一系列相关对象创建型建造者模式分步构建复杂对象创建型原型模式克隆已有对象结构型适配器模式转换接口让不兼容的类能一起工作结构型桥接模式分离抽象和实现结构型组合模式部分-整体树形结构结构型装饰器模式动态给对象添加职责结构型外观模式提供统一的高层接口结构型享元模式共享细粒度对象结构型代理模式控制对对象的访问行为型责任链模式链式处理请求行为型命令模式请求封装成对象行为型迭代器模式顺序访问集合元素行为型备忘录模式保存和恢复对象状态行为型观察者模式一对多依赖通知行为型状态模式对象状态决定行为行为型策略模式算法可以替换行为型模板方法模式父类定义算法骨架行为型访问者模式数据和操作分离行为型解释器模式定义一个语法的表示行为型中介者模式对象间通过中介通信三大型:创建型、结构型、行为型为什么GoF要把23种模式分成这三大类?这不是随意分的,而是按照解决的问题类型来划分的。

创建型:解决"对象怎么创建"面向对象编程里,"创建对象"看起来是最简单的事,但实践中会发现问题不少:

频繁创建和销毁对象会损耗性能创建对象需要知道具体类型,违反了"面向接口编程"的原则某些对象理论上应该只有一份(比如配置中心、线程池)创建型模式解决的就是"对象创建"这件事,它们让你在创建对象的时候更加灵活、可控。

结构型:解决"对象怎么组合"写代码的时候,你会发现对象之间是有层级和关联关系的。把多个类拼在一起形成一个更大的结构,这件事做不好就会导致代码耦合严重、难以维护。

结构型模式解决的是"对象和类如何组织成更大的结构",核心思路是通过组合而非继承来扩展功能——这在软件工程上叫"组合优于继承"原则。

行为型:解决"对象怎么协作"对象之间是有交互的,一个对象的行为往往会触发另一个对象的行为。责任链模式、观察者模式、命令模式……这些模式解决的是"对象之间的责任分配和行为协作"。

每种模式解决什么问题下面逐一说一下每种模式最典型的使用场景,帮助你在遇到实际问题的时候知道该往哪个方向想。

单例模式(Singleton)解决:某个类在整个系统里只能有一个实例。

典型场景:配置管理器、连接池、线程池、日志管理器。在这些场景下,如果出现多个实例,反而会导致资源竞争或者数据不一致。

工厂方法模式(Factory Method)解决:创建对象的时候,不想让调用方依赖具体的类,而是由子类来决定创建哪种对象。

典型场景:日志框架(你只调用LoggerFactory.getLogger,具体返回的是Logback还是Log4j2由配置决定)、JDBC驱动(你用DriverManager.getConnection,驱动是由MySQL还是PostgreSQL决定)。

抽象工厂模式(Abstract Factory)解决:需要创建一整套相关联的对象,这一整套对象必须是配套的。

典型场景:UI主题(Windows风格按钮+Windows风格窗口是一套,不能混用)、数据库访问层(MySQL的数据源和Oracle的数据源不能混用一套POJO)。

建造者模式(Builder)解决:构造函数的参数太多,或者有些参数可选,用普通构造函数会导致大量重载,代码难看且难维护。

典型场景:StringBuilder的append方法、Lombok的@Builder注解、Guava的CacheBuilder。常见的应用是创建一个有10个以上字段的DTO。

原型模式(Prototype)解决:创建对象的成本很高(比如需要从数据库或者网络加载数据),直接new一个代价太大,不如先克隆一个已有的对象再修改。

典型场景:原型设计、游戏里快速复制一个已构建好的游戏对象、文档模板的批量生成。

适配器模式(Adapter)解决:手头有两个接口不兼容的类,想让它们一起工作,但既不想改它们的代码也不想重新实现。

典型场景:Java I/O库(字节流和字符流之间的转换通过InputStreamReader/OutputStreamWriter适配)、对接第三方SDK(不同厂商的支付接口通过统一适配器转换)。

桥接模式(Bridge)解决:类的两个维度(比如说"形状"和"颜色")都可能独立变化,如果用继承来组合会导致类爆炸。

典型场景:跨平台的图形库(形状和绘制API是两个独立变化的维度)、消息发送系统(消息类型和发送渠道独立扩展)。

组合模式(Composite)解决:想用树形结构来处理"部分-整体"的关系,对单个对象和组合对象的操作要保持一致。

典型场景:文件系统(文件和文件夹都有"大小"这个概念)、GUI组件(容器和控件都可以被绘制和添加子组件)、组织架构系统。

装饰器模式(Decorator)解决:想在不修改原有类代码的前提下,动态给对象添加一些功能。

典型场景:Java I/O库是装饰器模式的经典应用——BufferedInputStream包装FileInputStream,给它加上缓冲功能,而BufferedInputStream本身又可以被DataInputStream包装。线上给方法加日志、加缓存、加事务,也是装饰器的思路。

外观模式(Facade)解决:系统内部很复杂,调用方不需要知道所有细节,给他们一个简单统一的入口就够了。

典型场景:医院挂号系统(病人只需要去挂号窗口,不需要知道医生怎么排班、药品怎么库存)、订单系统(一个createOrder方法背后可能调用了库存服务、支付服务、物流服务)。

享元模式(Flyweight)解决:大量细粒度对象占用内存过高,把其中共享的部分提取出来复用,可以大幅节省内存。

典型场景:文字编辑器里的字符对象(五万个字不需要五万个对象,每个字符的字体、字号可以共享)、游戏里的子弹(大量子弹共享同一种纹理和轨迹模型)。

代理模式(Proxy)解决:不能直接访问某个对象,或者想在访问它之前、之后做一些额外的操作。

典型场景:Spring AOP(方法增强)、MyBatis的延迟加载(访问关联对象时才真正查询数据库)、远程代理(RMI调用远程机器上的对象)。

责任链模式(Chain of Responsibility)解决:一个请求需要经过多个处理者依次处理,但具体由哪个处理者处理在编译时不确定。

典型场景: Servlet Filter(请求先过Filter链再到达Servlet)、审批流程(请假申请→主管→经理→HR逐级审批)、日志级别过滤。

命令模式(Command)解决:想把"请求"封装成对象,这样就可以对请求排队、记录日志、支持撤销操作。

典型场景:菜单系统(每个菜单项是一个Command,点击时执行)、任务队列(把任务封装成Command对象丢进队列里异步执行)、编辑器撤销(每个操作都是Command,Undo栈记录所有执行过的Command)。

迭代器模式(Iterator)解决:集合的内部结构不一样,但遍历的方式应该是统一的。

典型场景:Java的Iterator接口foreach循环、遍历链表和遍历数组用同一套语法。现在各大语言都内置了迭代器,语言层面的语法糖已经把迭代器模式变得透明了。

备忘录模式(Memento)解决:需要保存对象某个时刻的状态,之后可能需要恢复到这个状态。

典型场景:游戏存档、数据库事务回滚、编辑器的Undo功能。

观察者模式(Observer)解决:当一个对象的状态变化时,需要通知多个其他对象,并且不知道这些对象具体是谁。

典型场景:发布-订阅系统、消息推送、GUI事件机制(按钮点击事件)、Spring ApplicationEvent。MQ消息队列本质上也是观察者模式的升级版。

状态模式(State)解决:对象的行为取决于它的内部状态,而且它会在运行时根据状态改变行为。

典型场景:订单系统(待付款→已付款→已发货→已完成,每种状态下可执行的操作不同)、电梯控制(停止、运行、开门、关门)。最典型的就是状态机。

策略模式(Strategy)解决:同一个功能有多种实现算法,可以在运行时选择用哪一种。

典型场景:支付方式(支付宝、微信、银行卡)、排序算法(不同的数组规模用不同的排序策略)、校验规则(不同的用户类型用不同的校验逻辑)。

模板方法模式(Template Method)解决:某个算法的骨架是固定的,但某些步骤的具体实现可能不一样,把不变的部分放到父类,变的部分留给子类override。

典型场景:JDBC查询(连接→查询→处理结果→关闭,这几步骨架固定,但查询语句和结果处理各有不同)、Spring Data JPA(CrudRepository的save方法骨架固定,具体的insert还是update由JPA自动判断)。

访问者模式(Visitor)解决:数据结构稳定,但作用于数据上的操作经常变化。把操作和数据结构分离,操作者不需要知道数据的具体结构。

典型场景:编译器(语法树的节点结构固定,但每个节点的语义分析方式各不相同)、报表系统(人员结构和部门结构固定,但不同的报表格式需要不同的统计方式)。

解释器模式(Interpreter)解决:需要解释执行某种自定义语言或者表达式。

典型场景:正则表达式引擎、SQL解析、配置文件格式解析。这个模式在业务代码里很少直接用,一般都用成熟的工具库。

中介者模式(Mediator)解决:对象之间的引用关系很复杂,导致牵一发动全身。用一个中介对象来封装一组对象之间的交互。

典型场景:聊天室(每个用户不直接和其他用户通信,而是通过聊天室服务器转发)、机场调度系统(所有飞机不直接互相通信,都通过塔台调度)。

设计模式的背后原则GoF在总结这23种模式的时候,背后其实有一些共同的设计原则在支撑:

一、"针对接口编程,而不是针对实现编程"

这是整个设计模式生态的核心原则。不管是工厂方法、策略模式还是装饰器模式,它们的共同思路都是"抽象出一个接口,调用方依赖接口而非具体实现"。这样实现可以随时替换,系统就具备了可扩展性。

二、"优先使用组合而非继承"

继承是静态的,组合是动态的。装饰器模式、代理模式、桥接模式都在实践这个原则。通过组合把行为挂载上去,比写一个庞大的继承树灵活得多。

三、"开放-封闭原则"

对扩展开放,对修改封闭。好的设计应该是允许扩展的,但不需要修改已有的代码。策略模式、模板方法模式、装饰器模式都服务于这个目标。

四、"里氏替换原则"

子类可以扩展父类的功能,但不能改变父类原有的功能。这个原则是"针对接口编程"的延伸,也是确保继承关系稳定的基础。

这个系列要怎么学后面的23篇文章,每篇深入讲解一个设计模式,内容结构如下:

模式定义:它解决的是什么问题结构解读:类与类之间的关系是什么样的(配上UML风格的文字描述)代码实现:Java代码从零实现,足够完整可以跑起来使用场景:在什么情况下应该用它优点与缺点:没有银弹,它会带来什么代价与相关模式的对比:它和哪些模式容易混淆,区别在哪里每篇都保证4000字以上,有代码有图有分析,力求讲透。

如果你是在校学生,这个系列能帮你建立对设计模式的直觉理解。如果你是工作多年的工程师,这个系列能帮你把零散的知识串成体系,看源码的时候会突然有"原来这里用的是这个模式"的感悟。

建议的学习顺序是按系列文章顺序来,因为有些模式之间是有依赖关系的(比如装饰器模式和代理模式容易混淆,对比着学效果最好)。

从下一篇开始,我们正式进入23种模式的逐一深入讲解。

第一篇详细讲解:单例模式——最简单,也是面试最常问的模式,同时也是被误解最多的模式。