文章目录
-
- 设计模式简介
- 设计模式原则(SOLID)
- 一、创建型模式
-
- [1. 工厂模式](#1. 工厂模式)
- [2. 单例模式](#2. 单例模式)
- [3. 原型模式](#3. 原型模式)
- [4. 建造者模式](#4. 建造者模式)
- [5. 抽象工厂模式](#5. 抽象工厂模式)
- 二、结构型模式
-
- [6. 适配器模式](#6. 适配器模式)
- [7. 装饰器模式](#7. 装饰器模式)
- [8. 代理模式](#8. 代理模式)
- [9. 外观模式](#9. 外观模式)
- [10. 桥接模式](#10. 桥接模式)
- [11. 组合模式](#11. 组合模式)
- [12. 享元模式](#12. 享元模式)
- 三、行为型模式
-
- [13. 观察者模式(发布---订阅)](#13. 观察者模式(发布—订阅))
- [14. 策略模式](#14. 策略模式)
- [15. 模板方法模式](#15. 模板方法模式)
- [16. 职责链模式](#16. 职责链模式)
- [17. 命令模式](#17. 命令模式)
- [18. 状态模式](#18. 状态模式)
- [19. 迭代器模式](#19. 迭代器模式)
- [20. 备忘录模式](#20. 备忘录模式)
- [21. 中介者模式](#21. 中介者模式)
- [22. 访问者模式](#22. 访问者模式)
- [23. 解释器模式](#23. 解释器模式)
- 模式对比速查表
设计模式简介
设计模式是软件开发中反复出现的问题的通用解决方案,是经过大量实践验证的最佳实践总结。使用设计模式的核心价值:
- 重用代码 --- 避免重复造轮子
- 易于理解 --- 用公认的术语描述架构意图
- 保证可靠性 --- 经过验证的成熟方案
设计模式原则(SOLID)
| 原则 | 说明 | 白话理解 |
|---|---|---|
| S 单一职责 | 一个程序只做好一件事 | 功能复杂了就拆分 |
| O 开放封闭 | 对扩展开放,对修改封闭 | 加功能加新代码,别改老代码 |
| L 里氏替换 | 子类能覆盖父类 | 父类能出现的地方子类就能出现 |
| I 接口隔离 | 保持接口的单一独立 | 别让一个接口干所有事 |
| D 依赖倒转 | 面向接口编程 | 依赖抽象,不依赖具体实现 |
举个栗子 --- Promise 就体现了 S 和 O:
- 单一职责 :每个
.then()只做一件事 - 开放封闭 :新增需求只需追加
.then(),无需改动已有链
再来一个反例 → 正例的演化:
js
// ❌ 反例:每次加校验规则都要改函数内部 --- 违反开放封闭原则
const checkType = (str, type) => {
switch (type) {
case 'email': return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str)
case 'mobile': return /^1[3|4|5|7|8][0-9]{9}$/.test(str)
case 'tel': return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str)
default: return true
}
}
// ✅ 正例:开放扩展,封闭修改
const createValidator = () => {
const rules = {
email: str => /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str),
mobile: str => /^1[3|4|5|7|8][0-9]{9}$/.test(str),
}
return {
check: (str, type) => (rules[type] ? rules[type](str) : false),
addRule: (type, fn) => { rules[type] = fn },
}
}
const validator = createValidator()
validator.addRule('money', str => /^[0-9]+(\.[0-9]{2})?$/.test(str))
console.log(validator.check('18817023900', 'mobile')) // true
console.log(validator.check('18.36', 'money')) // true
一、创建型模式
1. 工厂模式
定义一个创建对象的接口,由子类决定实例化哪个类,将
new操作封装起来。
核心思想:把"创建什么对象"的决策从调用方移走,调用方只关心拿到的对象能干什么。
js
// 产品:用户
const createUser = (name, role) => {
const say = () => console.log(`我是${role}:${name}`)
return role === 'admin'
? { name, role, say, manage: () => console.log('管理系统权限') }
: { name, role, say }
}
// 工厂
const userFactory = {
create: (name, role) => createUser(name, role),
}
// 使用
const admin = userFactory.create('小明', 'admin')
const guest = userFactory.create('小红', 'guest')
admin.say() // 我是admin:小明
guest.say() // 我是guest:小红
现实中的例子:
js
// jQuery 的 $() 就是一个工厂 --- 根据传入参数不同创建不同的 jQuery 对象
const $ = (selector) => {
const el = typeof selector === 'string'
? document.querySelectorAll(selector)
: selector
return {
el,
addClass: (cls) => { /* ... */ },
on: (event, fn) => { /* ... */ },
}
}
| 优点 | 缺点 |
|---|---|
| 创建过程复杂时,调用方只需关心结果 | 简单场景用工厂反而多此一举 |
| 构造函数和创建者分离,符合开闭原则 | 增加产品时要加新的工厂函数 |
| 扩展性好 | 引入抽象层,增加理解成本 |
适用场景:需要根据环境创建不同实例,且这些实例有相同行为时。
什么时候不用 :如果只是简单的 new,直接用构造函数更清晰。
2. 单例模式
一个类只有一个实例,并提供全局访问点。
js
// 弹窗管理器 --- 整个应用只需要一个
const createModal = () => {
let modalEl = null
const show = (content) => {
if (modalEl) {
modalEl.style.display = 'block'
return
}
modalEl = document.createElement('div')
modalEl.className = 'modal'
modalEl.textContent = content
document.body.appendChild(modalEl)
}
const hide = () => {
if (modalEl) modalEl.style.display = 'none'
}
return { show, hide }
}
// 利用闭包 + IIFE 保证单例
const getModal = (() => {
let instance = null
return () => {
if (!instance) instance = createModal()
return instance
}
})()
// 使用
const m1 = getModal()
const m2 = getModal()
console.log(m1 === m2) // true --- 全局唯一
现实中的例子:
js
// Vuex Store --- 一个应用只有一个 store
// Redux createStore --- 单例 store
// 全局缓存、全局配置对象
const globalConfig = (() => {
const config = {}
return {
get: (key) => config[key],
set: (key, val) => { config[key] = val },
}
})()
| 优点 | 缺点 |
|---|---|
| 减少全局变量,增强模块性 | 强耦合,不利于单元测试 |
| 只实例化一次,节省内存 | 隐含的全局状态,调试困难 |
适用场景:登录弹窗、全局 store、数据库连接池、配置文件管理器。
3. 原型模式
用原型实例指定创建对象的种类,通过拷贝原型来创建新对象。
js
// 汽车原型
const carPrototype = {
wheels: 4,
drive() { console.log(`${this.brand} 正在行驶...`) },
}
// 基于原型创建新对象
const createCar = (brand) => {
const car = Object.create(carPrototype)
car.brand = brand
return car
}
const tesla = createCar('Tesla')
const bmw = createCar('BMW')
tesla.drive() // Tesla 正在行驶...
bmw.drive() // BMW 正在行驶...
console.log(tesla.wheels) // 4(来自原型)
现实中的例子:
js
// Object.create() 就是原型模式的直接应用
// JS 中 extends 本质上也是原型继承
// 配置模板 --- 创建多个带默认值的配置对象
const defaultConfig = { timeout: 3000, retry: 3, cache: true }
const apiConfig = { ...defaultConfig, url: '/api/users' } // 展开运算符也是拷贝原型的思想
| 优点 | 缺点 |
|---|---|
| 通过拷贝创建重复对象,提升性能 | 深拷贝嵌套对象可能复杂 |
| 简化新对象创建 |
适用场景:需要大量创建相似对象,且创建成本较高时。
4. 建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
核心思想:分步骤构建一个复杂对象,每一步只关注自己负责的部分,最终拼装出完整对象。
js
// 建造一个汉堡套餐
const createMealBuilder = () => {
const meal = {}
const builder = {
setMain: (main) => { meal.main = main; return builder },
setDrink: (drink) => { meal.drink = drink; return builder },
setSnack: (snack) => { meal.snack = snack; return builder },
build: () => ({ ...meal, describe: () =>
console.log(`套餐:${meal.main} + ${meal.drink} + ${meal.snack}`)
}),
}
return builder
}
// 使用
const mealA = createMealBuilder()
.setMain('汉堡')
.setDrink('可乐')
.setSnack('薯条')
.build()
mealA.describe() // 套餐:汉堡 + 可乐 + 薯条
现实中的例子:
js
// 表单构建器
const createFormBuilder = () => {
const fields = []
const builder = {
addInput: (label, name) => {
fields.push({ type: 'input', label, name })
return builder
},
addSelect: (label, name, options) => {
fields.push({ type: 'select', label, name, options })
return builder
},
build: () => ({ fields, render: () => console.log('渲染表单:', fields) }),
}
return builder
}
| 优点 | 缺点 |
|---|---|
| 构建过程清晰可控,链式调用语义好 | 代码量增加 |
| 构建与表示分离,可创建不同表示 | 不适合简单对象(杀鸡用牛刀) |
适用场景:需要构造包含多个可选参数的对象、构建步骤固定的复杂对象。
5. 抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
和普通工厂的区别 :普通工厂生产一种产品,抽象工厂生产一组相关产品(产品族)。
js
// 抽象工厂:UI 组件工厂 --- 为不同平台(Web / Mobile)创建一整套 UI 组件
const createWebUI = () => ({
button: (text) => ({ render: () => `<button>${text}</button>` }),
input: (placeholder) => ({ render: () => `<input placeholder="${placeholder}">` }),
})
const createMobileUI = () => ({
button: (text) => ({ render: () => `[MobileBtn ${text}]` }),
input: (placeholder) => ({ render: () => `[MobileInput hint="${placeholder}"]` }),
})
// 使用
const uiFactory = (platform) => platform === 'web' ? createWebUI() : createMobileUI()
const ui = uiFactory('web')
console.log(ui.button('提交').render()) // <button>提交</button>
现实中的例子:
js
// 不同数据库驱动返回一整套操作(connect, query, close)
const createMySQLDriver = () => ({
connect: () => console.log('MySQL 连接'),
query: (sql) => console.log(`MySQL 执行:${sql}`),
})
const createPgDriver = () => ({
connect: () => console.log('PostgreSQL 连接'),
query: (sql) => console.log(`Pg 执行:${sql}`),
})
| 优点 | 缺点 |
|---|---|
| 保证产品族内对象的一致性 | 增加新产品族结构时需要改动工厂接口 |
| 客户端与具体产品解耦 | 结构较复杂 |
二、结构型模式
6. 适配器模式
将一个接口转换成客户期望的另一个接口,使原本不兼容的接口可以一起工作。
核心思想:像电源转换插头一样,在中间做一层"翻译"。
js
// 新接口
const newRequest = ({ url, method = 'GET', data = {} }) =>
fetch(url, { method, body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } })
.then(res => res.json())
// 旧代码全用 $.ajax 的格式,做个适配器包一层
const $ = {
ajax: ({ url, type = 'GET', dataType = 'json', data = {} }) =>
newRequest({ url, method: type, data })
.then(res => ({
data: res,
status: 200,
})),
}
// 老代码不用改,直接用
$.ajax({ url: '/api/user', type: 'GET' }).then(({ data }) => console.log(data))
现实中的例子:
js
// Vue computed --- 把 data 适配成模板需要的格式
const vm = {
data: { firstName: 'Zhang', lastName: 'San' },
computed: {
fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`, // 适配!
},
}
// 把第三方 SDK 的数据格式转成自己项目的格式
const adaptUserData = (thirdPartyUser) => ({
id: thirdPartyUser.user_id,
name: thirdPartyUser.nickname,
avatar: thirdPartyUser.head_img_url,
})
| 优点 | 缺点 |
|---|---|
| 让不兼容的接口可以协作 | 增加了一层调用 |
| 提高了类的复用度 | 如果可以直接改原接口,就别用适配器 |
🆚 适配器 vs 代理 :适配器提供不同的接口 ,代理提供一模一样的接口。
7. 装饰器模式
不改变原对象,通过包装来动态添加功能。
核心思想:像给手机套壳 --- 手机本身没变,但多了保护功能。
js
// 原函数:提交订单
const submitOrder = (order) => {
console.log(`订单 ${order.id} 提交成功`)
}
// 装饰器:加上日志记录
const withLogging = (fn) => (...args) => {
console.log(`[${new Date().toISOString()}] 开始执行`)
const result = fn(...args)
console.log(`[${new Date().toISOString()}] 执行完成`)
return result
}
// 装饰器:加上性能监控
const withTimer = (fn) => (...args) => {
const start = Date.now()
const result = fn(...args)
console.log(`耗时:${Date.now() - start}ms`)
return result
}
// 叠加装饰
const enhancedSubmit = withTimer(withLogging(submitOrder))
enhancedSubmit({ id: 'A001' })
现实中的例子:
js
// React HOC(高阶组件)
const withAuth = (Component) => (props) => {
if (!props.isLogin) return <LoginPage />
return <Component {...props} />
}
// Express/Koa 中间件 --- 在请求前后添加行为
const logger = (handler) => async (req, res) => {
console.log(`${req.method} ${req.url}`)
return handler(req, res)
}
| 优点 | 缺点 |
|---|---|
| 动态扩展功能,比继承更灵活 | 多层装饰可能让调用链复杂 |
| 装饰者和被装饰者解耦 | 产生很多小对象 |
8. 代理模式
为对象提供一个替身,以控制对这个对象的访问。
核心思想:你想追一个女孩,不直接送花,而是通过她的闺蜜在最合适的时机转交。
js
// 原始图片加载
const loadImage = (src) => {
const img = new Image()
img.src = src
return img
}
// 代理:加占位图和懒加载
const createImageProxy = (src, placeholder = '/loading.gif') => {
const img = new Image()
img.src = placeholder // 先显示占位图
const realImg = new Image()
realImg.onload = () => { img.src = src } // 真实图片加载完再替换
realImg.src = src
return { img, getSrc: () => img.src }
}
现实中的例子:
js
// ES6 Proxy
const api = { getUser: (id) => fetch(`/api/user/${id}`) }
const apiWithCache = new Proxy(api, {
cache: {},
get(target, key) {
const fn = target[key]
return async (...args) => {
const cacheKey = `${key}_${JSON.stringify(args)}`
if (this.cache[cacheKey]) return this.cache[cacheKey]
const result = await fn(...args)
this.cache[cacheKey] = result
return result
}
},
})
// DOM 事件代理
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') console.log('点击了:', e.target.textContent)
})
| 优点 | 缺点 |
|---|---|
| 保护目标对象,控制访问 | 多一层间接调用,可能有性能差异 |
| 可扩展功能,符合开闭原则 |
🆚 代理 vs 装饰器 :代理是控制访问 (加限制),装饰器是扩展功能(不改变原有使用方式)。
9. 外观模式
为子系统的一组接口提供统一的高层接口,让子系统更容易使用。
核心思想:给你的复杂系统提供"一键操作"按钮。
js
// 电脑开机 --- 内部很复杂,对外只需要一个按钮
const cpu = { start: () => console.log('CPU 启动') }
const memory = { load: () => console.log('内存加载') }
const disk = { read: () => console.log('硬盘读取') }
// 外观 --- 一键开机
const powerButton = {
press: () => {
cpu.start()
memory.load()
disk.read()
console.log('✅ 开机完成')
},
}
powerButton.press()
现实中的例子:
js
// 兼容不同浏览器的事件绑定
const on = (el, event, handler) => {
if (el.addEventListener) return el.addEventListener(event, handler, false)
if (el.attachEvent) return el.attachEvent(`on${event}`, handler)
el[`on${event}`] = handler
}
// 统一封装的 fetch 工具
const http = {
get: (url, params) => fetch(`${url}?${new URLSearchParams(params)}`).then(r => r.json()),
post: (url, data) => fetch(url, { method: 'POST', body: JSON.stringify(data) }).then(r => r.json()),
put: (url, data) => fetch(url, { method: 'PUT', body: JSON.stringify(data) }).then(r => r.json()),
delete: (url) => fetch(url, { method: 'DELETE' }).then(r => r.json()),
}
| 优点 | 缺点 |
|---|---|
| 减少系统间依赖,提供简洁入口 | 不符合开闭原则,外观改变可能影响多处 |
| 提高安全性和易用性 |
10. 桥接模式
将抽象与实现分离,使它们可以独立变化。
核心思想:让"做什么"和"怎么做"各自独立,通过组合关联。
js
// "怎么做" --- 颜色实现
const colors = {
red: () => ({ name: '红色', hex: '#ff0000' }),
blue: () => ({ name: '蓝色', hex: '#0000ff' }),
green: () => ({ name: '绿色', hex: '#00ff00' }),
}
// "做什么" --- 形状抽象
const createShape = (type, color) => ({
draw: () => console.log(`绘制一个${color.name}(${color.hex})的${type}`),
})
// 组合使用
const red = colors.red()
const circle = createShape('圆形', red)
circle.draw() // 绘制一个红色(#ff0000)的圆形
现实中的例子:
js
// 渲染器 + 数据源 的桥接
// 图表库:renderer 可以在 Canvas/SVG 之间切换,dataProvider 可以在 REST/WebSocket 之间切换
const createChart = ({ renderer, dataProvider }) => ({
render: async () => {
const data = await dataProvider.fetch()
renderer.draw(data)
},
})
| 优点 | 缺点 |
|---|---|
| 抽象和实现可独立扩展 | 类/函数数量增加 |
| 提高可扩展性 |
11. 组合模式
将对象组合成树形结构,以表示"整体---部分"的层次关系,让客户端对单个对象和组合对象的使用保持一致。
js
// 文件系统 --- 文件和文件夹都能"显示大小"
const createFile = (name, size) => ({
name,
getSize: () => {
console.log(`${name}: ${size}KB`)
return size
},
})
const createFolder = (name) => {
const children = []
return {
name,
add: (item) => { children.push(item) },
getSize: () => {
const total = children.reduce((sum, c) => sum + c.getSize(), 0)
console.log(`${name} 文件夹总计: ${total}KB`)
return total
},
}
}
// 使用
const home = createFolder('home')
home.add(createFile('readme.md', 2))
home.add(createFile('package.json', 5))
const src = createFolder('src')
src.add(createFile('index.js', 20))
src.add(createFile('utils.js', 10))
home.add(src)
home.getSize()
// readme.md: 2KB
// package.json: 5KB
// index.js: 20KB
// utils.js: 10KB
// src 文件夹总计: 30KB
// home 文件夹总计: 37KB
现实中的例子:
js
// React 组件树 ------ 每个组件都是"可渲染"的,无论是叶子组件还是组合组件
const Button = (props) => render('button', props)
const Form = (props) => (
render('form', null,
props.children.map(child => child()) // 统一处理!
)
)
| 优点 | 缺点 |
|---|---|
| 客户端统一处理单个对象和组合对象 | 对象太多时系统可能负担较重 |
| 易于扩展,新增类型无需改动使用方 |
12. 享元模式
运用共享技术有效支持大量细粒度对象的复用,减少内存占用。
核心思想 :把大量相似对象的共享状态 (内部状态)抽出来共用,非共享状态(外部状态)在使用时传入。
js
// 驾考车 --- 只创建有限的几辆车,多考生排队复用
const createExamCarPool = (manualCount, autoCount) => {
const cars = []
let id = 0
// 创建车辆(享元)
const createCar = (type) => {
const carId = ++id
return {
carId,
type,
examine: (candidateId) => new Promise((resolve) => {
console.log(`考生-${candidateId} 在${type}车-${carId} 上考试`)
setTimeout(() => {
console.log(`考生-${candidateId} 考试完毕`)
resolve()
}, Math.random() * 2000 + 500)
}),
}
}
// 初始化车辆池
for (let i = 0; i < manualCount; i++) cars.push(createCar('手动档'))
for (let i = 0; i < autoCount; i++) cars.push(createCar('自动档'))
// 分配车辆:找空闲车
const queue = []
const assign = (candidateId) => {
const car = cars.find(c => !c.busy)
if (car) {
car.busy = true
car.examine(candidateId).then(() => {
car.busy = false
if (queue.length) assign(queue.shift()) // 考完立即分配给下一个人
})
} else {
queue.push(candidateId)
}
}
return { assign, queue }
}
// 3 辆车,10 个考生
const pool = createExamCarPool(2, 1)
for (let i = 1; i <= 10; i++) pool.assign(i)
| 优点 | 缺点 |
|---|---|
| 大幅减少对象数量,节省内存 | 需要区分内部/外部状态,增加复杂度 |
| 外部状态管理不当容易混乱 |
适用场景:大量相似对象导致内存紧张时(如在线表格的单元格、游戏中的粒子、文件上传列表)。
三、行为型模式
13. 观察者模式(发布---订阅)
定义一对多的依赖关系,当一个对象状态变化时,所有依赖者自动收到通知。
js
// 股票行情 --- 一个价格源,多个观察者
const createStock = (symbol, initPrice) => {
const observers = new Set()
let price = initPrice
return {
subscribe: (fn) => {
observers.add(fn)
return () => observers.delete(fn) // 返回取消订阅函数
},
getPrice: () => price,
setPrice: (newPrice) => {
if (newPrice !== price) {
price = newPrice
observers.forEach(fn => fn(price)) // 通知所有观察者
}
},
}
}
// 使用
const appleStock = createStock('AAPL', 150)
const unsubscribe = appleStock.subscribe((price) => {
console.log(`📊 收到更新:AAPL 现价 $${price}`)
})
appleStock.setPrice(152) // 📊 收到更新:AAPL 现价 $152
unsubscribe() // 取消订阅
appleStock.setPrice(155) // 无输出
现实中的例子:
js
// DOM 事件
document.addEventListener('click', () => console.log('clicked'))
// Vue 响应式 / EventEmitter / RxJS
// Redux store.subscribe()
const store = {
listeners: [],
subscribe: (fn) => { store.listeners.push(fn); return () => store.listeners = store.listeners.filter(l => l !== fn) },
dispatch: (action) => store.listeners.forEach(fn => fn(action)),
}
| 优点 | 缺点 |
|---|---|
| 广播通信,对象间解耦 | 过度使用会让调用链路难以追踪 |
| 灵活,可动态增减观察者 | 通知顺序可能不可控 |
14. 策略模式
定义一系列算法,封装起来使它们可以互相替换。算法的变化不会影响使用算法的客户端。
js
// 电商折扣计算 --- 不同折扣策略
const discountStrategies = {
normal: (price) => price,
vip: (price) => price * 0.9,
superVip: (price) => price * 0.8,
seasonal: (price) => price * 0.75,
}
const calcPrice = (price, strategy) =>
(discountStrategies[strategy] || discountStrategies.normal)(price)
// 使用
console.log(calcPrice(100, 'vip')) // 90
console.log(calcPrice(100, 'seasonal')) // 75
现实中的例子 --- 表单校验:
js
const validateStrategies = {
required: (val) => val.trim() ? '' : '必填',
minLen: (len) => (val) => val.length >= len ? '' : `至少${len}个字符`,
isEmail: (val) => /^[\w-]+@[\w-]+\.[\w-]+$/.test(val) ? '' : '邮箱格式不正确',
isMobile: (val) => /^1[3-9]\d{9}$/.test(val) ? '' : '手机号格式不正确',
}
// 校验工厂
const createValidator = (rules) => (value) => {
for (const [strategy, param] of rules) {
const fn = typeof param === 'function' ? param : validateStrategies[strategy](param)
const error = fn(value)
if (error) return error
}
return ''
}
// 使用
const validateName = createValidator([
['required'],
['minLen', 2],
])
console.log(validateName('')) // 必填
console.log(validateName('张')) // 至少2个字符
| 优点 | 缺点 |
|---|---|
| 避免多重 if-else / switch | 策略类/函数增多 |
| 符合开闭原则,算法可灵活替换 | 使用者需要了解各策略的区别 |
15. 模板方法模式
在一个方法中定义算法的骨架,将一些步骤延迟到子函数中实现。
js
// 饮品制作模板 --- 步骤固定,具体实现可变
const makeDrink = ({ brew, addFlavor }) => () => {
// 这些步骤是固定的
const boilWater = () => console.log('① 烧开水')
const pourIntoCup = () => console.log('③ 倒进杯子')
boilWater()
brew() // 可变步骤 1
pourIntoCup()
addFlavor() // 可变步骤 2
console.log('✅ 制作完成\n')
}
// 咖啡
const makeCoffee = makeDrink({
brew: () => console.log('② 冲泡咖啡粉'),
addFlavor: () => console.log('④ 加牛奶和糖'),
})
// 茶
const makeTea = makeDrink({
brew: () => console.log('② 浸泡茶叶'),
addFlavor: () => console.log('④ 加柠檬'),
})
makeCoffee()
makeTea()
现实中的例子:
js
// Vue/React 生命周期钩子 ------ 框架定义了渲染流程(模板方法),
// 你在 mounted/componentDidMount 里填充自己的逻辑
// Axios 拦截器也是模板方法的体现
| 优点 | 缺点 |
|---|---|
| 公共代码集中维护,避免重复 | 步骤固定,灵活性受限于模板定义 |
| 子函数只需关心自己那部分 |
16. 职责链模式
将请求沿链传递,直到有对象处理它为止。
js
// 请假审批链:组长(≤1天) → 经理(≤3天) → 总监(>3天)
const createApprover = (name, limit, next = null) => ({
handle: (days) => {
if (days <= limit) {
console.log(`${name} 批准了 ${days} 天假期`)
return true
}
if (next) return next.handle(days)
console.log(`无人能批准 ${days} 天假期`)
return false
},
})
const director = createApprover('总监', 10)
const manager = createApprover('经理', 3, director)
const leader = createApprover('组长', 1, manager)
leader.handle(1) // 组长 批准了 1 天假期
leader.handle(2) // 经理 批准了 2 天假期
leader.handle(5) // 总监 批准了 5 天假期
现实中的例子:
js
// DOM 事件冒泡 --- 点击 li,事件沿 li → ul → div → body 传递
// Koa/Express 中间件 --- 请求依次经过各中间件,某个中间件处理后就返回
// JS 原型链 --- 属性查找沿 __proto__ 向上
| 优点 | 缺点 |
|---|---|
| 请求发送者和接收者解耦 | 不能保证请求一定被处理 |
| 可动态调整链的顺序和成员 | 长链可能带来性能问题 |
17. 命令模式
将请求封装成对象,使你可以用不同的请求参数化客户端,支持排队、日志、撤销。
js
// 遥控器 --- 每个按钮是一个命令
const createTV = () => {
let on = false
let volume = 10
return {
turnOn: () => { on = true; console.log('📺 电视开了') },
turnOff: () => { on = false; console.log('📺 电视关了') },
volumeUp: () => { volume++; console.log(`🔊 音量:${volume}`) },
volumeDown: () => { volume = Math.max(0, volume - 1); console.log(`🔉 音量:${volume}`) },
}
}
// 命令调度器 --- 支持撤销
const createRemote = (tv) => {
const history = []
const execute = (command) => {
const [action, undo] = command
action()
history.push({ action, undo })
}
const undo = () => {
if (!history.length) return console.log('没有可撤销的操作')
const cmd = history.pop()
cmd.undo()
}
return { execute, undo }
}
// 定义命令(执行 + 撤销 配对)
const commands = (tv) => ({
on: [() => tv.turnOn(), () => tv.turnOff()],
off: [() => tv.turnOff(), () => tv.turnOn()],
up: [() => tv.volumeUp(), () => tv.volumeDown()],
down: [() => tv.volumeDown(), () => tv.volumeUp()],
})
const tv = createTV()
const remote = createRemote(tv)
const cmd = commands(tv)
remote.execute(cmd.on) // 📺 电视开了
remote.execute(cmd.up) // 🔊 音量:11
remote.execute(cmd.up) // 🔊 音量:12
remote.undo() // 🔉 音量:11
remote.undo() // 🔊 音量:10
| 优点 | 缺点 |
|---|---|
| 命令和执行者解耦 | 命令类/对象增多 |
| 支持撤销、重做、命令队列 |
适用场景:编辑器撤销/重做、任务队列、宏命令录制。
18. 状态模式
对象内部状态改变时,其行为也随之改变。看起来就像换了类一样。
js
// 交通灯 --- 状态切换逻辑
const createTrafficLight = () => {
const states = {
red: {
color: '🔴',
duration: 3000,
next: () => 'green',
},
green: {
color: '🟢',
duration: 3000,
next: () => 'yellow',
},
yellow: {
color: '🟡',
duration: 1000,
next: () => 'red',
},
}
let current = 'red'
let timer = null
const run = () => {
const state = states[current]
console.log(`${state.color} ${current.toUpperCase()} --- 持续${state.duration / 1000}秒`)
timer = setTimeout(() => {
current = state.next()
run()
}, state.duration)
}
const stop = () => clearTimeout(timer)
return { run, stop, getState: () => current }
}
// 使用:
// const light = createTrafficLight()
// light.run() // 🔴 → 🟢 → 🟡 → 🔴 ...
现实中的例子:
js
// Promise 的 pending → fulfilled / rejected 状态转换
// 订单状态:待付款 → 已付款 → 已发货 → 已收货 → 已完成
const createOrder = () => {
const states = {
pending: { pay: () => 'paid', cancel: () => 'cancelled' },
paid: { ship: () => 'shipped', refund: () => 'refunding' },
shipped: { receive: () => 'completed' },
completed: {},
cancelled: {},
}
let state = 'pending'
return {
getState: () => state,
transition: (action) => {
const next = states[state]?.[action]
if (!next) return console.log(`❌ 当前[${state}]不能执行[${action}]`)
state = next
console.log(`✅ ${action} → ${state}`)
},
}
}
| 优点 | 缺点 |
|---|---|
| 消除大量 if-else,状态逻辑清晰 | 状态类变多 |
| 状态切换一目了然 |
19. 迭代器模式
提供一种顺序访问集合元素的方法,而不暴露集合的内部结构。
js
// 自定义迭代器
const createIterator = (arr) => {
let index = 0
return {
next: () => (index < arr.length
? { value: arr[index++], done: false }
: { value: undefined, done: true }
),
}
}
const it = createIterator([10, 20, 30])
let result = it.next()
while (!result.done) {
console.log(result.value) // 10, 20, 30
result = it.next()
}
现实中的例子:
js
// ES6 内置:Array.prototype[Symbol.iterator]
// for...of 就是基于迭代器协议
// Generator 函数
const range = function* (start, end) {
for (let i = start; i <= end; i++) yield i
}
for (const n of range(1, 5)) console.log(n) // 1, 2, 3, 4, 5
| 优点 | 缺点 |
|---|---|
| 统一遍历接口,不暴露内部结构 | 简单遍历使用 forEach 更直接 |
| 支持不同数据结构用同一套算法 |
20. 备忘录模式
在不破坏封装的前提下,捕获对象内部状态并在外部保存,以便后续恢复。
js
// 文本编辑器 --- 支持撤销
const createEditor = () => {
let content = ''
const history = [] // 备忘录栈
let cursor = -1
const save = () => {
// 保存当前状态
history.length = cursor + 1 // 清除"未来"历史
history.push(content)
cursor++
}
return {
type: (text) => {
save()
content += text
console.log(`📝 当前内容: "${content}"`)
},
undo: () => {
if (cursor <= 0) return console.log('无法撤销')
cursor--
content = history[cursor]
console.log(`↩ 撤销后: "${content}"`)
},
redo: () => {
if (cursor >= history.length - 1) return console.log('无法重做')
cursor++
content = history[cursor]
console.log(`↪ 重做后: "${content}"`)
},
getContent: () => content,
}
}
const editor = createEditor()
editor.type('Hello')
editor.type(' World')
editor.undo() // "Hello"
editor.undo() // ""
editor.redo() // "Hello"
| 优点 | 缺点 |
|---|---|
| 提供状态恢复机制 | 保存大量状态会占用内存 |
| 不破坏封装性 |
适用场景:编辑器撤销/重做、游戏存档、表单草稿保存。
21. 中介者模式
用一个中介对象封装一系列对象之间的交互,使各对象不需要显式地相互引用。
js
// 聊天室 --- 中介者转发消息,用户之间不直接通信
const createChatRoom = () => {
const users = {}
return {
register: (user) => { users[user.name] = user; user.setRoom(this) },
send: (from, to, msg) => {
if (to) {
// 私聊
users[to]?.receive(from, msg)
} else {
// 广播
Object.keys(users).forEach(name => {
if (name !== from) users[name].receive(from, msg)
})
}
},
}
}
const createUser = (name) => {
let room = null
return {
name,
setRoom: (r) => { room = r },
send: (to, msg) => room.send(name, to, msg),
receive: (from, msg) => console.log(`[${name}] 收到 ${from}: ${msg}`),
}
}
const room = createChatRoom()
const alice = createUser('Alice')
const bob = createUser('Bob')
const carol = createUser('Carol')
room.register(alice)
room.register(bob)
room.register(carol)
alice.send('', '大家好!') // 广播
bob.send('Alice', '你好 Alice') // 私聊
| 优点 | 缺点 |
|---|---|
| 网状多对多 → 一对多,降低耦合 | 中介者本身可能变得臃肿 |
| 各对象独立,易于复用 |
22. 访问者模式
在不改变元素类的前提下,定义作用于元素的新操作。
js
// 对不同的员工类型执行不同的操作(计算薪资、生成报表)
const employeeTypes = {
engineer: (name, salary) => ({
type: 'engineer',
name,
salary,
accept: (visitor) => visitor.visitEngineer({ name, salary }),
}),
manager: (name, salary, bonus) => ({
type: 'manager',
name,
salary,
bonus,
accept: (visitor) => visitor.visitManager({ name, salary, bonus }),
}),
}
// 访问者:计算年终奖
const bonusCalculator = {
visitEngineer: (e) => e.salary * 2,
visitManager: (m) => m.salary * 3 + m.bonus,
}
// 访问者:生成工资条
const payslipGenerator = {
visitEngineer: (e) => `${e.name}(工程师): 月薪 ${e.salary}`,
visitManager: (m) => `${m.name}(经理): 月薪 ${m.salary} + 奖金 ${m.bonus}`,
}
// 使用
const alice = employeeTypes.engineer('Alice', 15000)
const bob = employeeTypes.manager('Bob', 20000, 5000)
console.log(alice.accept(bonusCalculator)) // 30000
console.log(bob.accept(bonusCalculator)) // 65000
console.log(alice.accept(payslipGenerator)) // Alice(工程师): 月薪 15000
| 优点 | 缺点 |
|---|---|
| 数据结构和操作分离,扩展操作方便 | 元素需要暴露内部信息给访问者 |
| 符合单一职责 | 新增元素类型时需要修改所有访问者 |
23. 解释器模式
定义一种语言的文法表示,并提供一个解释器来解释这种语言中的句子。
js
// 简易计算器解释器 --- 解析 "数字 操作符 数字"
const tokenize = (input) => {
const tokens = []
const regex = /\d+|[+\-*/]/g
let match
while ((match = regex.exec(input)) !== null) tokens.push(match[0])
return tokens
}
const interpret = (tokens) => {
// 数字解析
const number = (token) => {
const num = parseInt(token, 10)
if (isNaN(num)) throw new Error(`"${token}" 不是有效数字`)
return num
}
// 操作符解析
const operators = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => {
if (b === 0) throw new Error('除数不能为 0')
return a / b
},
}
const operator = (token) => {
const fn = operators[token]
if (!fn) throw new Error(`未知运算符: ${token}`)
return fn
}
// 执行:左 → 右,不考虑优先级,以演示为目的
let result = number(tokens[0])
for (let i = 1; i < tokens.length; i += 2) {
const op = operator(tokens[i])
const num = number(tokens[i + 1])
result = op(result, num)
}
return result
}
// 使用
const calculate = (expr) => interpret(tokenize(expr))
console.log(calculate('3 + 5 * 2')) // 16(先算 3+5=8,再算 8*2=16,无优先级)
console.log(calculate('10 / 2 + 3')) // 8
| 优点 | 缺点 |
|---|---|
| 易于扩展语法规则 | 复杂文法时性能较差 |
| 复杂文法难以维护,应考虑用现成的解析器 |
适用场景:SQL 解析、正则表达式、模板引擎、自定义 DSL。
模式对比速查表
| 对比项 | 模式 A | 模式 B | 核心区别 |
|---|---|---|---|
| 创建对象 | 工厂 | 建造者 | 工厂一步创建;建造者分步构建 |
| 创建对象 | 工厂 | 抽象工厂 | 工厂造一种产品;抽象工厂造一族产品 |
| 包装对象 | 适配器 | 代理 | 适配器改接口;代理不改接口 |
| 包装对象 | 代理 | 装饰器 | 代理控制访问;装饰器增强功能 |
| 解耦 | 观察者 | 中介者 | 观察者一对多广播;中介者一对多集中管理 |
| 解耦 | 策略 | 状态 | 策略外部选择算法;状态内部切换行为 |
| 结构 | 组合 | 享元 | 组合组织树形结构;享元共享实例减少内存 |
总结 :设计模式不是银弹,而是工具箱。关键是在合适场景选用合适的模式,不要为了用模式而用模式。理解每种模式要解决的问题,比记住它的写法更重要。