如果你打开搜索引擎搜“设计模式”,被 23 个模式的类图和各种语言的实现刷屏,第一反应是关掉页面,这太正常了。我在团队里带过不同基础的同事,也在面试里看过太多“背得熟”却“用不出”的候选人。设计模式的问题从来不是难学,而是学习顺序错了:它不是一本需要背诵的类图字典,而是一类反复出现的问题的成熟解法。这篇我会用一个老程序员的工作视角,把模式的底层逻辑、三类 23 个模式的主线、Java/C++/C# 里的落地差异,以及软考、期末、大作业、面试的答题框架一次讲清楚,尽量让零基础的人也知道从哪下手、怎么用起来。
1. 先别急着背 23 个模式,搞清楚它们到底在解决什么问题
1.1 模式是路标,不是菜谱
很多人一上来就搜“设计模式简介”,然后被“类、对象、耦合、复用”这些词劝退。我特别理解。我自己刚学的时候,也对着 GOF 那本《设计模式》头晕,满脑子都是“我怎么可能背下来 23 个”。后来工作久了才发现,设计模式根本不是用来背的,而是用来“认”的。
Erich Gamma 等四人总结的 23 个模式,本质上是把软件工程几十年里反复出现的设计问题,以及被验证过的解决骨架,归纳成了几组。它们就像路标:看到“适配器”,你该想到两边接口对不上;看到“代理”,你该想到访问控制、延迟加载或者日志;看到“观察者”,你该想到一个状态变了,一群人要响应。没有这些路标,你也能靠硬编码把功能写出来,但代码会越来越拧巴,改一个需求牵一发动全身。
你可以把设计模式理解成装修工程的“标准工序”。每套房子户型不同,但水电怎么走、防水怎么做、瓷砖怎么铺,都有成熟的套路。你不需要每次从零发明一套,而是先判断我的情况属于哪种典型场景,再套用对应的成熟做法。设计模式的价值就在这里:把重复踩坑的经验沉淀成可复用的结构。
1.2 零基础的正确学习姿势:场景、冲突、方案
我见过不少同学直接去背“工厂模式有几个角色、类图怎么画”,背完就忘。因为那是从结论出发,没有经历问题本身。更好的学习顺序是:场景 → 冲突 → 方案。
举个最常见的例子。你要写一个支付模块,一开始只支持微信支付,代码很简单,一个类搞定。三个月后要加支付宝,你可能会写 if/else。再加银联、银行卡,这个 if/else 会越来越长,每次新增渠道都要改老代码,测试也越来越痛苦。这时候你发现的问题,就是工厂模式和策略模式要解决的问题。你带着这个痛点去学,一学就会,而且永远不会忘。
所以我的建议是三步走:
- 复现:每个模式用最小代码写一遍,不追求背类图,而是体会模式的骨架。
- 辨识:在别人的代码里找模式。Spring、JDK、.NET、C++ 标准库里到处都是模式,认出来比写出来更重要。
- 改造:在真实需求里刻意用一次。哪怕一开始有点过度设计,也没关系,关键是体会“如果没有这个模式,改动会发生在哪里”。
很多人“背了 23 个,期末考完就忘”,就是因为只做到第一步。设计模式是技能,不是知识点。技能需要肌肉记忆,需要在真实的代码里反复触发。
2. 真正的地基:SOLID 原则才是你选模式的尺子
2.1 五个原则的大白话版本
在开始分门别类讲模式之前,我强烈建议你先理解 SOLID 原则。因为模式是“术”,原则是“道”。经常有新手问我:这个场景该用工厂还是策略?其实答案藏在原则里:你到底要隔离哪种变化?
- 单一职责原则(SRP):一个类最好只有一个引起它变化的原因。不要把订单计算、日志、持久化全部塞进一个 OrderService,后面改任何一块都会误伤另外两块。
- 开闭原则(OCP):对扩展开放,对修改关闭。加新功能时,尽量不修改已经写好并且测试过的旧代码。比如加优惠策略时,不去改原来的打折方法,而是新增一个策略类。
- 里氏替换原则(LSP):凡是用父类的地方,换成子类不能出问题。经典的“正方形继承长方形”反例就是:宽高单独设置时,正方形的行为会破坏父类约束。
- 接口隔离原则(ISP):客户端不应该依赖它用不到的接口。与其放一个大而全的接口,不如拆成几个小接口。
- 依赖倒置原则(DIP):面向抽象编程,不面向具体实现编程。业务代码依赖 PayChannel 接口,而不是依赖 WeChatPay 实现类。
这五个原则不是考试背诵题,而是代码评审时的判断依据。你在 code review 里说“这个类太长了”“这个接口太胖了”“这里不该 new 具体的实现”,本质上都是在用 SOLID 说话。
2.2 原则是怎么导向模式的
原则本身不会直接告诉你用哪个模式,但它们会指出变化点。我自己的判断习惯是:先问“这段代码将来最可能因为什么而变”,再顺着变化点找模式。
举个例子:
- 如果变化点在“对象的创建方式”,比如类型经常增加、构造参数很复杂,优先考虑工厂方法、抽象工厂、建造者。
- 如果变化点在“行为算法”,比如折扣、运费、税率规则经常调整,优先考虑策略模式。
- 如果变化点在“通知关系”,比如订单状态一变,短信、邮件、积分都要跟着动,优先考虑观察者。
- 如果变化点在“接口形态”,比如要兼容老系统、第三方 SDK 的接口,优先考虑适配器。
- 如果变化点在“访问控制”,比如某些接口只允许特定角色调用,优先考虑代理。
你可以用下面这张表快速定位:
| 原则 | 它真正在问什么 | 常见关联模式 |
|---|---|---|
| 单一职责 | 这个类会不会因为多个原因被修改 | 状态、策略、观察者 |
| 开闭原则 | 加功能时旧代码动不动 | 工厂、策略、装饰器、观察者 |
| 里氏替换 | 子类能不能无缝替换父类 | 模板方法、策略 |
| 接口隔离 | 接口是不是最小可用 | 适配器、外观 |
| 依赖倒置 | 业务是否依赖抽象 | 抽象工厂、工厂方法、依赖注入 |
每次纠结“该用哪个模式”的时候,不要盯着模式列表看,回到问题本身:最需要被隔离的变化点是什么。想清楚这一句,模式自然浮出来。
3. 创建型模式:把“对象怎么来的”从业务逻辑里拆出去
3.1 单例模式:最容易背,最容易用错
单例是面试里非常爱考的小题,因为它看起来简单,坑却非常多。它的目标是保证一个类只有一个实例,并提供全局访问点。常见场景是配置中心、连接池、日志对象。
Java 里一个线程安全的懒汉单例要写成这样:
public class Config { private static volatile Config instance; private Config() {} public static Config getInstance() { if (instance == null) { synchronized (Config.class) { if (instance == null) { instance = new Config(); } } } return instance; } }volatile不能省,否则在并发下可能拿到半初始化对象。C++ 从 C++11 开始,最简单的方式是“局部静态变量”:
class Logger { public: static Logger& getInstance() { static Logger instance; return instance; } Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; private: Logger() = default; };C# 直接用Lazy<T>最省心。面试时你能说出这三种语言的差异,基本上单例这块就过关了。
但我要提醒你一句:单例是全局可变状态的温床。多人协作时,大家都会往单例里塞数据,测试用例之间互相污染,排查问题非常痛苦。能用依赖注入把实例传进去,就别直接去拿单例。
3.2 工厂方法家族:从“简单工厂”到“抽象工厂”
简单工厂其实不属于 GOF 的 23 个模式,但它是最常见的第一步:用一个方法集中创建对象。
public class PayFactory { public PayChannel create(String type) { if ("alipay".equals(type)) { return new AlipayChannel(); } if ("wechat".equals(type)) { return new WechatChannel(); } throw new IllegalArgumentException("unknown pay type"); } }这段代码的优点是调用方不用关心具体类,缺点是每次加渠道都要改这个 if/switch,违反开闭原则。想彻底解决,就升级成工厂方法模式:把create做成抽象方法,每种渠道一个子工厂,新增渠道时新增一个子工厂,不动旧代码。
抽象工厂再进一步:它负责创建“一族”相关的对象。比如一个 UI 主题,按钮、输入框、弹窗必须是配套风格。你要深色主题,就有一个 DarkThemeFactory 同时产出深色按钮和深色弹窗;要浅色主题,就有 LightThemeFactory。关键词是“产品族”。
面试时经常被问“工厂和抽象工厂有什么区别”,一句话回答:工厂方法解决“一个产品”的创建,抽象工厂解决“一族产品”的配套创建。
3.3 建造者与原型:复杂对象的两种快捷方式
建造者模式适合构造参数特别多、并且希望对象创建后不可变的场景。比如组装一台电脑,直接 new 一个几十个参数的构造函数,谁看谁崩溃。用 Builder 链式调用:
Computer c = new Computer.Builder() .cpu("i7-13700K") .gpu("RTX 4070") .ram("32G") .disk("2T SSD") .build();建造者和工厂的区别是:工厂关心“造出什么类型”,建造者关心“一步步把复杂对象拼好”。很多框架里的Builder就是这种模式,比如 Lombok 的@Builder、OkHttp 的Request.Builder。
原型模式适合创建成本高的对象。你有一份默认的商品模板,每次新商品都复制一份再改少量字段,而不是从零建。注意浅拷贝和深拷贝:Java 的clone()默认是浅拷贝,C++ 要自己写拷贝构造,否则两个对象会共享内部数组,改一个等于改了全部。我在项目中一般优先推荐使用拷贝工厂或序列化复制,少依赖语言原生 clone,逻辑更透明。
创建型模式的口诀可以是:单工建原抽。对应单例、工厂方法、抽象工厂、建造者、原型,五个一起记,期末考试分类题基本不会丢分。
4. 结构型模式:类和对象的组装方式,决定你改代码是轻松还是难受
4.1 适配器、装饰器、代理:三个长得像,职责完全不一样
结构型模式里最容易被混淆的三个是适配器、装饰器、代理,因为它们都是“在一个类外面再包一层”。但包的目的完全不同。
- 适配器解决接口不匹配。现实类比是电源转换头,三脚插头转两脚。代码里典型例子是
InputStreamReader,它把一个字节流InputStream适配成字符流Reader,两边不用改。 - 装饰器解决能力增强。它给对象动态叠加功能,不改原类。Java IO 是最好例子:
new BufferedInputStream(new FileInputStream(...)),BufferedInputStream给原始文件流加了缓冲能力。你还可以继续包DataInputStream,加读取基本类型的能力。 - 代理解决访问控制。代理类和目标类实现同一个接口,外面看起来一样,但真正干活前可以加权限校验、延迟加载、日志记录。Spring AOP 的动态代理就是典型。
一句话区分:接口对不上用适配器,功能要叠加用装饰器,对象不能直接碰用代理。面试时用这三句话开头,再补个例子,基本就稳了。
4.2 外观、组合、桥接、享元
这四个在结构型里也很有用。
外观模式是给复杂子系统开一个简洁的门面。比如你写一个OrderFacade,把库存扣减、支付、物流三个服务串起来,调用方只需要调placeOrder(),不需要知道内部有五个类在协作。它降低的是使用方的认知成本。
组合模式解决树形结构。文件夹里有文件和子文件夹,但客户端可以统一调用getName(),不用区分当前是文件还是文件夹。菜单、目录树、组织架构图都可以用它。关键是让单个对象和组合对象实现同一个接口。
桥接模式解决“两个维度独立变化”的问题。比如图形有圆形、方形,绘制引擎有 OpenGL、DirectX。如果你用继承,每加一个图形就要多好几个子类。桥接的做法是:图形类里组合一个绘制引擎接口,画图时委托给引擎。抽象与实现各自扩展,互不干扰。
享元模式是批量共享不可变状态,省内存。Java 的Integer缓存 -128 到 127、String 常量池,都是享元思想。用共享时要注意把“内蕴状态”和“外蕴状态”分开:内蕴状态可以共享,外蕴状态由外部传入。
结构型的共同点是尽量用对象组合代替继承。组合比继承更灵活,代价是代码变绕、间接层变多。所以结构型模式不是用得越多越好,而是发现类出现爆炸式增长时才需要。
5. 行为型模式:把“动作和交互”变成可插拔的策略
5.1 策略、状态、模板方法:处理行为变化的三个主力
策略模式解决“算法替换”。比如优惠计算:满减、折扣、赠品,每次新增规则,不去改主业务类的 if/else,而是新增一个策略类。调用方只面向一个DiscountStrategy接口,运行时切换具体实现。用 Java 写,可以简化成:
public interface DiscountStrategy { double apply(double amount); }C++ 里甚至不需要定义接口,直接用一个std::function<double(double)>就能当策略:
std::function<double(double)> discount = [](double price) { return price * 0.8; };状态模式和策略模式长得有点像,但意图不同。策略是客户端主动选算法;状态是对象内部状态自动决定行为。最典型的例子是订单状态:待支付、已支付、已发货、已取消,每个状态一个类,状态类自己决定下一步流转。主类不再写几十个 if/else。
模板方法模式定义一个流程骨架,把变化步骤留给子类。比如“准备饮品”的流程是固定的:煮水、冲泡、加料。子类决定加茶还是加咖啡。框架代码里特别多,Spring 的JdbcTemplate就是模板方法:它帮你管理连接、执行 SQL、处理异常,只把 SQL 留给你。
5.2 观察者、责任链、命令
观察者模式解决一对多通知。按钮点击监听、股票价格变化通知、订单状态广播,都是观察者。Java 的PropertyChangeListener、C# 的event关键字,都是语言层面已经内置好的观察者。你写业务时,只要遇到“一个变化要让多个模块响应”,就可以考虑它。
责任链模式让请求沿着链条传递,每个节点决定处理还是放行。审批流、日志过滤器、Servlet Filter,都是责任链。好处是解耦发送者和处理者,坏处是链太长时排查链路很痛苦,所以我一般会控制节点数量,并打印清晰的调用日志。
命令模式把请求封装成对象。编辑器里的撤销功能是经典场景:每个操作都是一个 Command,执行完入栈,撤销时反向执行。命令模式还能支持操作队列、操作日志,适合做“可回放”的系统。
5.3 剩下的迭代器、中介者、备忘录、解释器、访问者怎么记
这五个属于“特定场景专用”,平时用得不多,但考试会考。
- 迭代器:把遍历和容器解耦。客户端不知道集合内部是数组还是链表,也能一个个遍历。C++ STL 的 iterator、Java 的 Iterator 都是。
- 中介者:多个对象直接交互变成通过中间人交互。聊天室是典型:客户端不直接互聊,都发给服务器转发。
- 备忘录:保存对象某个时刻的状态,用于恢复。注意保存和恢复都要控制权限,别把内部状态全部对外暴露。
- 解释器:定义一种语言并解释它。编译器、规则引擎会用到。除非你在做 DSL,否则不建议业务代码主动用它。
- 访问者:数据结构和操作分离。给元素集合增加新操作时,不用改每个元素类,而是加一个 Visitor。代价是新增元素类型时,所有 Visitor 都要改,所以只在结构稳定、操作常变的场景用。
行为型模式的口诀可以这样串:责命解迭中备观状策模访。对应责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者。配合前面的创建型、结构型口诀,正好凑齐 23 个,软考和期末的分类题都有救。
6. 从 Java、C++、C# 到 MVC:同一个模式在不同语言里的落地差异
6.1 Java:设计模式已经被框架吸收
Java 世界里,现在很少需要你手写设计模式,因为框架和语言已经替你做了。Spring 里到处是模式:Bean 默认是单例;BeanFactory 是工厂;AOP 是动态代理;JdbcTemplate、RestTemplate 是模板方法;事件监听是观察者。你去面试“Java 设计模式”相关的问题,与其背 23 个定义,不如先把 Spring 里用了哪些模式说清楚,含金量高得多。
另外,Java 8 之后的 Lambda 和函数式接口让策略模式写法轻量了很多。以前要写一个接口、一个实现类,现在经常一个 Lambda 表达式就解决了。模式思想没变,表达成本大幅下降。
6.2 C++:资源管理优先,部分模式写法完全不同
C++ 里设计模式要时刻想着资源管理,因为裸指针和手动内存管理会让模式变形。单例最简单可靠的写法是 C++11 的局部静态变量,线程安全由语言保证,拷贝构造要 delete 掉。组合、聚合关系中,强烈建议用unique_ptr或shared_ptr表达所有权,这样对象析构时资源能自动释放。
C++ 的策略模式也不一定要写抽象基类。可以用模板在编译期选择策略,性能更高,但代码可读性会下降;也可以用std::function在运行期替换,灵活性和 Java 接口类似。C++ 里常用的设计模式,反而更偏“惯用法”而不是 GOF 类图,比如 RAII、Pimpl、类型擦除。RAII 虽然不是 GOF 模式,但在 C++ 里是所有模式的地基:资源获取即初始化,析构函数负责释放。
6.3 C#:语言语法糖让模式实现更轻
C# 对设计模式非常友好。单例用Lazy<T>就够;观察者直接可以用event关键字;策略可以用Func、Action委托;LINQ 本身就是迭代器模式加链式调用;IDisposable加using语法接近 RAII。
所以 C# 开发者写设计模式,最怕的不是不会,而是重复造轮子。你要先问自己:这个模式是不是已经被语言或框架覆盖了?例如要遍历集合,直接用foreach和 LINQ,没必要自己实现迭代器。模式的价值在业务层,不在基础设施层。
6.4 MVC 是架构模式,不是 GOF 模式
MVC 经常被归进设计模式讨论,但它属于架构模式,比 GOF 设计模式大一个层级。MVC 内部也用了设计模式:视图和模型通过观察者保持同步,控制器可以看作策略,视图树可以看作组合模式。所以面试时被问到“MVC 和设计模式什么关系”,别只说三层结构,把观察者、策略、组合这几个关联点讲出来,面试官会觉得你是真的理解,而不是背概念。
顺带提一下最近网上那个“体验「设计创意模式」”的热词。它更多是产品设计、创意方法领域的说法,强调把用户洞察转成可复制的创新套路。它和软件里的 GOF 设计模式不是一个体系,但底层逻辑同源:把成功经验沉淀成可复用的套路,降低从 0 到 1 的决策成本。你写简历或者面试提到“设计模式”,默认还是指 GOF 这套,别被概念混淆带偏。
7. 软考、期末、大作业、面试:怎么答题才拿分
7.1 软考设计模式速记:场景关键词优先
软考上午题常给你一段场景描述,问你该用哪种模式,或者给出模式分类。我的速记方法是记“场景关键词”,而不是记完整定义。
| 场景关键词 | 优先选的模式 |
|---|---|
| 接口不兼容、老系统对接 | 适配器 |
| 对象只能一个 | 单例 |
| 创建对象类型不固定 | 工厂方法 |
| 一族配套产品 | 抽象工厂 |
| 算法可以替换 | 策略 |
| 对象状态自动流转 | 状态 |
| 一对多通知 | 观察者 |
| 树形目录、菜单 | 组合 |
| 动态增强功能 | 装饰器 |
| 不直接访问目标 | 代理 |
| 多层次请求处理 | 责任链 |
看到场景先判断变化点,再把模式名写出来,正确率会高很多。下午案例题如果让你补类图,就往“角色”上靠:模式由哪些类组成、类之间是什么关系、哪个类负责接口定义、哪个类负责实现。
7.2 期末和设计模式大作业:别堆模式,讲清楚“为什么”
期末画 UML 类图时,要注意关系符号:继承是实线空心三角,实现是虚线空心三角,依赖是虚线箭头,关联是实线箭头,聚合是空心菱形,组合是实心菱形。类图回答的是“谁和谁是什么关系”,画之前先想清楚。
设计模式大作业,我见过太多同学为了展示能力,硬塞八个模式进一个系统,最后老师看完一头雾水。我的建议是选一个具体小业务,比如网上订餐,挑三到四个模式就够了:
- 菜品创建用工厂模式:菜单类型增加时不需要改客户端;
- 订单状态通知用观察者模式:下单后自动通知后厨、推送用户、生成积分;
- 优惠计算用策略模式:满减、折扣、VIP 价可以自由切;
- 数据访问用模板方法:统一处理连接、事务、异常。
每个模式都要解释“如果没有它,改动会发生在哪里”。你只要把这个逻辑讲清楚,分就不会低。
7.3 面试题怎么答才有说服力
面试里高频的设计模式问题有:单例有几种写法;工厂和抽象工厂区别;策略和状态区别;代理和装饰器区别;Spring 中用了哪些设计模式;如何设计一个可扩展的优惠系统。
答题建议用三步框架:
- 讲场景:什么情况下会出现这个问题;
- 讲方案:我选择哪个模式,核心结构是什么;
- 讲权衡:这个模式带来什么好处,代价是什么。
比如问到策略模式,不要只说“把算法封装起来”。你可以说:优惠规则经常变,如果写死在业务类里,每次上线都改老代码,测试面很大。我用策略接口加实现类隔离变化,新增规则只需要加类;代价是会多出不少类,规则很少时不值得。
回答时主动说缺点特别加分。因为它能证明你不是背答案,而是真的在工程里权衡过。
8. 什么时候故意不用设计模式,以及我的实战习惯
8.1 模式是有成本的
每引入一个模式,就多一层间接。间接能解耦,也会让新手看不懂。项目只有两百行代码,一个 if/else 就能表达清楚的时候,上策略模式就是过度设计。
过度设计有几个信号:类数量开始明显多于业务实体;为了“统一”而造的抽象没有实际使用方;改一个简单逻辑要跨五六个文件;团队新人看代码时不停问“这个接口为什么存在”。出现这些信号,通常不是模式不够多,而是模式用重了。
8.2 我自己的三条底线
第一,第一次写代码时用最直白的方式,让需求先跑通。第二次遇到同一个变化点,再考虑抽象。如果第三次又在同一个位置改,那就说明这里需要模式了。我把它叫“三次规则”。
第二,选型先问变化点。创建方式变用工厂,行为算法变用策略,接口不匹配用适配器,状态流转用状态,通知关系用观察者。对准变化点再动手,不要看哪个模式名字好听。
第三,模式是给下一个改代码的人用的。如果团队里没人能看懂,这个模式就是负债。写注释时也别堆“这里是策略模式”,而要写“这里用策略是因为优惠算法经常独立变化”。解释意图,比解释类图有用得多。
如果只从这篇里带走一句话,我希望是:设计模式不是为了显得专业,而是为了让三个月后的你、换班的同事、后来接手的陌生人,改代码时少掉几根头发。我也是在真正被自己的代码坑过几次之后,才彻底想明白这件事的。