设计模式图解
从"背UML"到"看懂画面 + 代码对应" — 8大高频设计模式 × SVG结构图 × IDE实战
1单例模式 Singleton
一个国家只有一个总统——不管多少次选举,总统职位只有一个。无论多少个线程调用 getInstance(),拿到的都是同一个对象。
Singleton.js
// 1. 饿汉式 — 模块级常量,JS 单线程无需同步
class EagerSingleton {
constructor() {
if (EagerSingleton.INSTANCE) {
throw new Error('Use EagerSingleton.getInstance()');
}
}
static INSTANCE = new EagerSingleton();
static getInstance() {
return EagerSingleton.INSTANCE;
}
}
// 2. 懒汉式 — JS 单线程,无需 DCL/volatile,闭包封装
const LazyDCLSingleton = (function() {
let instance;
class LazyDCLSingleton {
constructor() {
if (instance) {
throw new Error('Use LazyDCLSingleton.getInstance()');
}
}
}
return {
getInstance() {
if (!instance) { // 首次检查
instance = new LazyDCLSingleton();
}
return instance;
}
};
})();
// 3. 冻结对象单例 — JS 无枚举,用 Object.freeze 模拟
const EnumSingleton = Object.freeze({
doSomething() {
console.log('枚举单例');
}
});
真实项目:Spring Bean 默认 scope="singleton";数据库连接池(HikariDataSource);Logger(slf4j)都是单例。Spring 的单例是基于 IoC 容器管理的,不是传统的类加载机制。
单例模式面试坑点:① 饿汉式类加载就创建,可能浪费资源;② 懒汉式不加 synchronized 线程不安全;③ DCL 必须加 volatile(防止指令重排序:new 操作不是原子的,分配内存→初始化→引用赋值,重排后可能返回未初始化的对象);④ 反射可破坏单例(枚举单例天然防反射);⑤ 序列化/反序列化会创建新实例(需实现 readResolve())。
2工厂模式 Factory
买车——你只说"我要一辆SUV",4S店(工厂)负责生产具体型号。你不需要知道车怎么组装的,只需要告诉工厂类型。
FactoryPattern.js
// 简单工厂 — 违反开闭原则,但简单实用
class CarFactory {
static create(type) {
switch (type) {
case 'suv': return new SUVCar();
case 'sedan': return new SedanCar();
default: throw new Error('Unknown type');
}
}
}
// 工厂方法 — 每个产品对应一个工厂子类(JS 无接口,注释约定)
// interface CarFactory { create() }
class SUVCarFactory { // implements CarFactory
create() { return new SUVCar(); }
}
// Spring FactoryBean — Spring专用的工厂模式(仅作参考,JS 无对应概念)
// Spring @Component
class MyFactoryBean { // implements FactoryBean
getObject() { return new MyService(); }
getObjectType() { return MyService; }
}
真实项目:Spring BeanFactory / ApplicationContext 是最大的工厂;LoggerFactory.getLogger();MyBatis 的 SqlSessionFactory。
工厂模式选择指南:产品只有2-3种且不太会新增 → 简单工厂(if-else够了);产品经常新增 → 工厂方法(符合开闭原则);需要创建一整套产品族 → 抽象工厂(如跨数据库切换 DAO 全家桶)。面试时一定要结合具体场景说,不要只背概念。
工厂模式三级对比
| 维度 | 简单工厂 | 工厂方法 | 抽象工厂 |
| 工厂个数 | 1个 | 多个(每产品一个) | 多个(每产品族一个) |
| 产品维度 | 单个产品 | 单个产品 | 产品族(多个产品) |
| 开闭原则 | ❌ 违反 | ✅ 符合 | ✅ 符合 |
| 新增产品 | 改工厂类代码 | 加一个工厂类 | 加一个工厂类+产品族 |
| 复杂度 | 低 | 中 | 高 |
| Spring应用 | — | FactoryBean | 切换数据源DAO |
3观察者/发布订阅模式 Observer / Pub-Sub
关注UP主——UP发新视频,所有粉丝收到通知。观察者模式是直接通知粉丝,发布订阅则是通过B站(调度中心)间接通知。
ObserverPattern.js
// Node.js EventEmitter 机制(发布订阅,对标 Spring Event)
// 1. 定义事件(JS 中事件就是普通对象)
class OrderCreatedEvent {
constructor(source, order) {
this.source = source;
this.order = order;
}
}
// 2. 发布事件 — 用 EventEmitter 替代 Spring ApplicationEventPublisher
// Spring @Service
class OrderService {
constructor(emitter) {
this.emitter = emitter; // 替代 @Autowired ApplicationEventPublisher
}
createOrder(order) {
// 保存订单...
this.emitter.emit('orderCreated', new OrderCreatedEvent(this, order));
}
}
// 3. 监听事件 — 可多个订阅者
// Spring @Component
class EmailNotifier {
constructor(emitter) {
emitter.on('orderCreated', (event) => { // 替代 @EventListener
// 发送邮件通知
});
}
}
// Vue3 EventBus(mitt库)
// emitter.ts
import mitt from 'mitt'
export const emitter = mitt()
// 发布
emitter.emit('orderCreated', { id: '123' })
// 订阅
emitter.on('orderCreated', (data) => {
console.log('收到通知', data)
})
JDK 的 Observable/Observer 在 JDK 9 中已废弃,推荐用 Spring Event 或 java.beans.PropertyChangeListener 替代。
观察者 vs 发布订阅 何时选择?
| 场景 | 推荐模式 | 原因 |
| 同一进程内,组件间通信 | 观察者 | 简单直接,无中间层 |
| 跨模块/跨服务解耦 | 发布订阅 | 调度中心解耦,可跨进程 |
| Spring应用内事件 | 发布订阅 | ApplicationEventPublisher 充当调度中心 |
| Vue/React 组件通信 | 发布订阅 | EventBus / Redux store 中转 |
| 微服务间消息 | 发布订阅 | MQ(Kafka/RabbitMQ)就是调度中心 |
4策略模式 Strategy
出行策略——赶时间选飞机,省钱选火车,锻炼选步行,策略随时切换。核心是"算法族,分别封装,互相替换"。
StrategyPattern.js
// 支付策略接口(JS 无接口,用注释约定 + 鸭子类型)
// interface PayStrategy { getType(); pay(amount); }
// Spring @Component
class WechatPayStrategy { // implements PayStrategy
getType() { return 'wechat'; }
pay(amount) { /* 微信支付 */ }
}
// Spring @Component
class AlipayStrategy { // implements PayStrategy
getType() { return 'alipay'; }
pay(amount) { /* 支付宝支付 */ }
}
// 手动注册策略到 Map(模拟 Spring @Autowired Map 注入)
// Spring @Service
class PayService {
constructor() {
this.strategyMap = new Map([
['wechat', new WechatPayStrategy()],
['alipay', new AlipayStrategy()]
]);
}
pay(type, amount) {
const strategy = this.strategyMap.get(type);
strategy.pay(amount);
}
}
真实项目:Spring 的 Resource 接口(ClassPathResource/FileSystemResource/UrlResource)就是策略模式;Comparator 是排序策略。
策略模式消除 if-else 的完整步骤
// Step 0: if-else 地狱(改造前)
pay(type, amount) {
if (type === 'wechat') {
wechatPayService.pay(amount);
} else if (type === 'alipay') {
alipayPayService.pay(amount);
} else if (type === 'card') {
cardPayService.pay(amount);
}
// 新增支付方式?继续加 else if 😱
}
// Step 1: 定义策略接口(JS 用注释约定)
// interface PayStrategy { getType(); pay(amount); }
// Step 2: 每种方式实现策略
// Spring @Component('wechat')
class WechatPayStrategy { ... } // implements PayStrategy
// Spring @Component('alipay')
class AlipayStrategy { ... } // implements PayStrategy
// Step 3: 手动注册到 Map(模拟 Spring 自动注入)
// Spring @Service
class PayService {
constructor() {
this.payStrategyMap = new Map([
['wechat', new WechatPayStrategy()],
['alipay', new AlipayStrategy()]
]);
}
pay(type, amount) {
this.payStrategyMap.get(type).pay(amount);
}
// 新增支付方式?加个类并注册即可,零修改 ✅
}
5装饰器模式 Decorator
穿衣服——T恤 → 外套 → 围巾,每层添加新功能,不改变原来的你。装饰器和被装饰者实现同一接口,层层包装。
DecoratorPattern.js
// Node.js Stream 装饰链 — JS 版的 I/O 装饰器
import { createReadStream } from 'fs'
import { createGzip } from 'zlib'
const stream = createReadStream('data.txt') // 被装饰者
.pipe(createGzip()); // 装饰器: 压缩
// 调用链: read() → gzip transform → 输出
// Stream 通过 pipe 串联,类似装饰器层层包装
// Express middleware 装饰链(类似 Spring HandlerInterceptor)
// Spring @Component
function authMiddleware(req, res, next) {
// 前置增强:鉴权
return next(); // return true
}
// JS 装饰器语法(TC39 提案,需 babel 支持)
function log(target, key, desc) {
const orig = desc.value
desc.value = function(...args) {
console.log(`调用 ${key}`)
return orig.call(this, ...args)
}
}
class Svc {
@log doWork() { }
}
真实项目:Java I/O 全家桶(InputStream/OutputStream/Reader/Writer);Spring AOP 代理装饰;Vue mixin 混入增强。
Java I/O 装饰链全景
6代理模式 Proxy
明星经纪人——你见不到明星本人,但经纪人可以帮你转达。经纪人还能加价(前置增强)和收尾(后置增强),而你毫不知情。
ProxyPattern.js
// JS Proxy — 原生动态代理(类似 JDK 动态代理)
const proxy = new Proxy(target, {
get(obj, prop) {
const orig = obj[prop];
if (typeof orig !== 'function') return orig;
return (...args) => {
// 前置增强
console.log('before: ' + prop);
const result = orig.apply(obj, args); // 调用目标
// 后置增强
console.log('after: ' + prop);
return result;
};
}
});
// JS Proxy 代理类实例(类似 CGLIB 代理,无需接口)
const realSubject = new RealSubject();
const cglibProxy = new Proxy(realSubject, {
get(obj, prop) {
const orig = obj[prop];
if (typeof orig !== 'function') return orig;
return (...args) => {
console.log('cglib before');
const result = orig.apply(obj, args);
console.log('cglib after');
return result;
};
}
});
// Spring AOP @AspectJ 注解方式(仅作参考,JS 无对应注解)
// Spring @Aspect @Component
class LogAspect {
// Spring @Before('execution(* com.example.service.*.*(..))')
before() { /* 前置通知 */ }
// Spring @AfterReturning('execution(* com.example.service.*.*(..))')
afterReturning() { /* 后置通知 */ }
}
真实项目:Spring AOP(JDK动态代理/CGLIB);RPC远程代理(调用远程方法像调用本地方法);MyBatis MapperProxy(接口没有实现类,代理生成SQL执行)。
代理模式 vs 装饰器模式 深度对比
| 维度 | 代理 Proxy | 装饰器 Decorator |
| 核心目的 | 控制对象访问 | 动态增强功能 |
| 创建者 | 代理方创建被代理对象 | 客户端组合装饰链 |
| 客户端是否知情 | 不知道(透明代理) | 知道(主动装饰) |
| 包装层数 | 通常1层 | 可多层嵌套 |
| 典型场景 | 远程代理、延迟加载、权限控制 | Java I/O、拦截器链 |
| Spring体现 | AOP @Aspect → 动态代理 | Interceptor chain → 装饰器链 |
| 面试话术 | "控制访问,经纪人模式" | "增强功能,穿衣服模式" |
面试高频坑:Spring AOP 既有代理思想也有装饰器思想。代理体现在:客户端不知道代理存在,代理控制对象访问。装饰器体现在:拦截器链层层增强,类似装饰器嵌套。建议先答代理模式,再补充"从拦截器链角度看也有装饰器思想"。
7适配器模式 Adapter
出国充电——中国插头插不进欧洲插座,加一个转换头(适配器)。适配器让两个不兼容的接口协同工作。
AdapterPattern.js
// 类适配器(JS 无接口,注释约定)
class ClassAdapter extends Adaptee { // implements Target
request() {
super.specificRequest(); // 直接调用父类
}
}
// 对象适配器(推荐)
class ObjectAdapter { // implements Target
constructor(adaptee) {
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest(); // 委派给 adaptee
}
}
// Spring HandlerAdapter — 适配不同类型的Handler(仅作参考)
// interface HandlerAdapter { supports(handler); handle(req, ...); }
class HandlerAdapter {
supports(handler) { /* ... */ }
handle(req) { /* ... */ }
}
// RequestMappingHandlerAdapter 适配 @RequestMapping
// HttpRequestHandlerAdapter 适配 HttpRequestHandler
真实项目:Arrays.asList() 把数组适配成 List;Spring MVC HandlerAdapter 适配多种 Handler;InputStreamReader 把 InputStream 适配成 Reader。
适配器模式在 Spring MVC 中的真实应用
8模板方法模式 Template Method
做菜模板——烧油 → 炒菜 → 调味,具体每步做什么菜不同(猪肉白菜 vs 牛肉土豆),但步骤模板固定。父类定骨架,子类填细节。
TemplateMethodPattern.js
// 模板方法模式(JS 无 abstract,用抛错模拟抽象方法)
class AbstractCook {
// 模板方法 — 定义算法骨架(JS 无法 final,约定不覆写)
cook() {
this.heatOil();
this.stirFry();
this.season();
if (this.wantGarnish()) { // 钩子方法
this.garnish();
}
}
heatOil() { throw new Error('子类必须实现 heatOil'); }
stirFry() { throw new Error('子类必须实现 stirFry'); }
season() { throw new Error('子类必须实现 season'); }
wantGarnish() { return false; } // 钩子默认实现
garnish() {} // 可选覆写
}
// Spring AbstractApplicationContext.refresh() — 经典模板方法(仅作参考)
// refresh() 定义了12个步骤,子类可覆写某些步骤:
// prepareRefresh() → obtainBeanFactory() → ...
// → finishRefresh()
// JS 回调式模板(类似 Spring JdbcTemplate 的回调模式)
function withConnection(dataSource, callback) {
const conn = dataSource.getConnection();
try {
return callback(conn); // 用户只需提供 SQL + 回调
} finally {
conn.close(); // 模板负责关闭连接
}
}
const users = withConnection(dataSource, (conn) => {
return conn.query('SELECT * FROM user').map(row => row.name);
});
// withConnection 封装了: 获取连接 → 执行回调 → 关闭连接
// 用户只需要提供 SQL 处理逻辑(回调)
真实项目:Spring AbstractApplicationContext.refresh() 是最经典的模板方法,定义了容器刷新的12个步骤;JdbcTemplate 封装了 JDBC 样板代码;HttpServlet 的 doGet()/doPost() 也是模板方法的变体。
Spring refresh() 模板方法拆解
// AbstractApplicationContext.refresh() — 模板方法(简化版,JS 风格)
refresh() {
this.prepareRefresh(); // 步骤1: 准备工作
const bf = this.obtainFreshBeanFactory(); // 步骤2: 创建BeanFactory
this.prepareBeanFactory(bf); // 步骤3: 配置BeanFactory
this.postProcessBeanFactory(bf); // 步骤4: 子类可覆写 👈 钩子
this.invokeBeanFactoryPostProcessors(bf); // 步骤5: 后置处理器
this.registerBeanPostProcessors(bf); // 步骤6: 注册BeanPostProcessor
this.initMessageSource(); // 步骤7: 国际化
this.initApplicationEventMulticaster(); // 步骤8: 事件广播器
this.onRefresh(); // 步骤9: 子类可覆写 👈 钩子
this.registerListeners(); // 步骤10: 注册监听器
this.finishBeanFactoryInitialization(bf);// 步骤11: 初始化单例Bean
this.finishRefresh(); // 步骤12: 完成刷新
}
// 子类如 AnnotationConfigServletWebServerApplicationContext
// 可覆写 onRefresh() 来创建内嵌 Tomcat 👈 典型的钩子方法
汇总
设计模式分类总表
| 类型 | 模式 | 一句话 | Spring中的应用 | 面试频率 |
| 创建型 | 单例 Singleton | 全局唯一实例,节省资源 | Bean 默认 singleton scope | ★★★★★ |
| 创建型 | 工厂 Factory | 将创建逻辑封装,客户端不关心细节 | BeanFactory / FactoryBean | ★★★★★ |
| 行为型 | 观察者 Observer | 一对多依赖,状态变化自动通知 | ApplicationEvent + @EventListener | ★★★★ |
| 行为型 | 策略 Strategy | 算法族分别封装,运行时可替换 | Resource 接口 / @Autowired Map | ★★★★ |
| 结构型 | 装饰器 Decorator | 动态添加功能,不改变原对象 | HandlerInterceptor / AOP包装 | ★★★★ |
| 结构型 | 代理 Proxy | 控制访问,增强而不改变接口 | AOP (JDK/CGLIB代理) | ★★★★★ |
| 结构型 | 适配器 Adapter | 接口转换,让不兼容的类协作 | HandlerAdapter | ★★★ |
| 行为型 | 模板方法 Template | 父类定骨架,子类填细节 | AbstractApplicationContext.refresh() | ★★★★ |
三种结构型模式易混淆对比
| 维度 | 装饰器 Decorator | 代理 Proxy | 适配器 Adapter |
| 目的 | 增强功能(加料) | 控制访问(经纪人) | 接口转换(转换头) |
| 对象创建 | 运行时层层组合 | 代理方控制创建 | 已有类需要适配 |
| 接口关系 | 与被装饰者同接口 | 与目标对象同接口 | 转换到目标接口 |
| 客户端感知 | 知道装饰链的存在 | 不知道代理的存在 | 通过新接口使用 |
| 层数 | 可多层嵌套 | 通常一层 | 通常一层 |
| Spring应用 | HandlerInterceptor | AOP / MapperProxy | HandlerAdapter |
| 类比 | 穿衣服(层层加) | 经纪人(替你办事) | 充电转换头 |
面试速答
Q: "Spring AOP用了什么设计模式?"
代理模式 + 装饰器模式。Spring AOP 的核心是动态代理(JDK 动态代理或 CGLIB 代理),在运行时为目标对象生成代理类,在代理类中织入增强逻辑(前置/后置/环绕通知)。从增强功能角度看,也有装饰器的思想——但不改变原对象接口。面试时先答代理模式,再补充装饰器角度,体现深度。
Q: "工厂模式和简单工厂区别?"
简单工厂是一个工厂类用 if-else/switch 决定创建哪种产品,违反开闭原则,新增产品要改工厂代码。工厂方法是每个产品对应一个工厂子类,新增产品只需新增工厂类,符合开闭原则。简单工厂够用时不要过度设计——3个以下产品用简单工厂就行。抽象工厂更进一步,创建产品族(一组相关产品),如跨数据库时切换整套 DAO。
Q: "观察者和发布订阅什么区别?"
核心区别在于是否有调度中心。观察者模式中 Subject 直接维护观察者列表并 notify,发布者和订阅者有直接依赖;发布订阅模式引入 EventBus/Topic 调度中心,发布者和订阅者完全解耦。Spring Event 是发布订阅模式(经 ApplicationEventPublisher 中转),Vue 的响应式系统本质是观察者模式(Dep 直接 notify Watcher)。
Q: "策略模式怎么消除 if-else?"
三步走:① 定义策略接口(如 PayStrategy),② 每种情况实现一个策略类(WechatPayStrategy、AlipayStrategy),③ 用 Map 自动注入——Spring 中 @Autowired Map<String, PayStrategy> 会自动将所有实现注入,key 是 bean 名。调用时 strategyMap.get(type).pay() 一行搞定,新增支付方式只需加一个 @Component 类,零修改原有代码。
Q: "装饰器和代理模式有什么区别?"
目的不同:装饰器是"增强功能",像穿衣服层层加功能;代理是"控制访问",像经纪人不让你直接见明星。客户端感知不同:装饰器客户端通常主动组合装饰链;代理客户端不知道代理存在。层数不同:装饰器可多层嵌套(BufferedInputStream 套 FileInputStream);代理通常就一层。Spring AOP 表面是代理,但拦截器链是装饰器思想的体现。
Q: "你项目里用过哪些设计模式?"
按频率回答:① 策略模式——支付方式/折扣策略,用 @Autowired Map 自动注入消除 if-else;② 模板方法——导出报表(PDF/Excel/CSV)步骤固定,具体格式不同;③ 工厂模式——不同渠道的消息发送器(短信/邮件/推送);④ 观察者/发布订阅——订单创建后触发库存扣减、积分发放、消息通知。关键:每个模式都要结合具体业务场景说,不要干巴巴念定义。
实战建议
面试说设计模式要结合 Spring 具体例子。不要说"我知道单例模式",要说"Spring Bean 默认是单例的,但和传统的饿汉式不同,Spring 的单例是由 IoC 容器管理的,每个 ApplicationContext 中只有一个实例"。有场景有细节,才显得你真正用过。
不要过度设计——3个以上 if-else 才考虑策略模式,2个的时候简单 if-else 更清晰。设计模式是为了解决问题,不是为了炫技。先写直白的代码,重构时再引入模式,这才是正常的工作流。
代理模式是理解 Spring AOP 和 MyBatis 的关键。Spring AOP 用 JDK 动态代理(有接口)或 CGLIB(无接口),MyBatis 的 MapperProxy 让你只写接口就能执行 SQL。理解了代理,Spring 框架的很多"魔法"就不再神秘了。