设计模式详解

41 阅读13分钟

设计模式详解(生活类比 + 原生 JavaScript 示例)


目录

  1. 创建型模式

  2. 结构型模式

  3. 行为型模式


使用说明

  • 每个模式都用同样的结构:
    • 一句话定义
    • 生活类比(贴近日常生活)
    • 解决什么问题
    • 什么时候用
    • 原生 JS 示例代码
  • 建议阅读顺序:单例 → 工厂 → 策略 → 观察者/发布订阅 → 代理 → 外观 → 其他

创建型模式

1. 单例模式 (Singleton)

一句话定义

保证一个类在整个程序中只有一个实例,并提供一个全局访问点。

生活类比
  • 整个城市只有一个「110 报警中心」,大家打电话都是连到同一个地方。
  • 公司只有一个「财务系统」,所有部门都往这一个系统里录数据。
解决什么问题
  • 多个地方各自创建同一种对象,导致数据不统一、状态不同步。
  • 配置/日志等“全局信息”需要集中管理。
什么时候用
  • 全局配置中心:环境变量、接口地址等。
  • 全局日志器:统一打印日志。
  • 全局缓存/存储:简单的 in-memory 缓存。
JS 示例:配置中心单例
class Config {
  constructor() {
    // 如果已经创建过一次,直接返回同一个实例
    if (Config.instance) {
      return Config.instance
    }

    // 第一次创建时初始化默认配置
    this.env = 'prod'
    this.apiHost = 'https://api.example.com'

    Config.instance = this
  }

  setEnv(env) {
    this.env = env
  }
}

const c1 = new Config()
const c2 = new Config()

console.log(c1 === c2)        // true,总是同一个对象
console.log(c1.env, c2.env)   // prod prod

c1.setEnv('dev')
console.log(c2.env)           // dev,c2 也跟着变

2. 工厂模式 (Factory)

一句话定义

不在外面自己 new,而是通过一个“工厂函数/类”来创建对象,由工厂决定创建哪种具体对象。

生活类比
  • 你点外卖:只选「汉堡套餐 / 披萨套餐」,后厨怎么组合主食+饮料+小食,你不用管。
  • 买手机套餐:「学生套餐 / 商务套餐」,具体包含的服务由运营商决定。
解决什么问题
  • 对象创建逻辑复杂、到处复制粘贴。
  • 需要根据不同条件创建不同“类型”的对象。
什么时候用
  • 有一类东西“长得差不多但略有不同”,例如不同类型的用户、不同类型的通知、不同支付渠道对象等。
JS 示例:用户工厂
function createUser(role) {
  if (role === 'admin') {
    return { role, permissions: ['read', 'write', 'delete'] }
  }
  if (role === 'editor') {
    return { role, permissions: ['read', 'write'] }
  }
  if (role === 'guest') {
    return { role, permissions: ['read'] }
  }
  // 默认
  return { role: 'guest', permissions: ['read'] }
}

const admin = createUser('admin')
const guest = createUser('guest')

3. 建造者模式 (Builder)

一句话定义

把一个复杂对象的创建过程拆成很多小步骤,通过链式调用一步一步“组装”出来。

生活类比
  • 点奶茶:先选杯型 → 选甜度 → 选冰量 → 选加料 → 确认下单。
  • 买车:选车系 → 选颜色 → 选轮毂 → 选内饰 → 选配置包。
解决什么问题
  • 构造函数参数太多,new Something(a, b, c, d, e, f) 可读性极差。
  • 需要可选配置很多、但每次用到的只是一部分。
什么时候用
  • 配置类对象(请求配置、表单配置、图表配置等)。
  • 需要链式调用构建数据结构的场景。
JS 示例:奶茶建造者
class DrinkBuilder {
  constructor() {
    this.drink = {
      type: '奶茶',
      size: '中杯',
      sugar: '正常糖',
      ice: '正常冰',
      toppings: []
    }
  }

  setSize(size) {
    this.drink.size = size
    return this
  }

  noSugar() {
    this.drink.sugar = '无糖'
    return this
  }

  lessIce() {
    this.drink.ice = '少冰'
    return this
  }

  addTopping(topping) {
    this.drink.toppings.push(topping)
    return this
  }

  build() {
    return this.drink
  }
}

const order = new DrinkBuilder()
  .setSize('大杯')
  .noSugar()
  .lessIce()
  .addTopping('珍珠')
  .addTopping('椰果')
  .build()

console.log(order)

结构型模式

4. 适配器模式 (Adapter)

一句话定义

写一层“转换器”,把一个接口/数据的格式,转换成另一种需要的格式。

生活类比
  • 国外电器的插头插不进国内插座,要用一个「转换插头」。
  • 朋友讲方言,你听不懂,需要一个“翻译”帮你转成普通话。
解决什么问题
  • 老接口、第三方库的返回格式和自己项目想用的不一样。
  • 新旧系统切换时,想尽量少改老代码。
什么时候用
  • 接了一个旧系统/三方 SDK,返回字段名和你内部统一规范对不上。
  • 接口升级,新老字段并存,需要中间层做统一。
JS 示例:用户数据适配器
// 老系统返回的数据
const oldUser = {
  name: '张三',
  age: 20,
  phone: '13800000000'
}

// 新系统期望的数据格式
function adaptUser(old) {
  return {
    username: old.name,
    age: old.age,
    mobile: old.phone,
    isAdult: old.age >= 18
  }
}

const newUser = adaptUser(oldUser)
console.log(newUser)

5. 装饰器模式 (Decorator)

一句话定义

在不改变原对象/函数代码的前提下,给它“套一层壳”增加新的功能。

生活类比
  • 一杯美式咖啡:可以加奶、加糖浆、加奶泡,每加一样都包了一层“装饰”,原咖啡还在里面。
  • 你给礼物包装:加盒子、加丝带、加贺卡,礼物本身没变,只是外面多了东西。
解决什么问题
  • 想给已有函数加日志、异常捕获、耗时统计等功能,但不想/不能改原函数。
  • 不同功能可以灵活组合,而不是写很多继承子类。
JS 示例:给函数加日志装饰
function add(a, b) {
  return a + b
}

// 装饰器:给任意函数增加日志能力
function withLog(fn) {
  return function (...args) {
    console.log('[LOG] 调用参数:', args)
    const result = fn(...args)
    console.log('[LOG] 返回结果:', result)
    return result
  }
}

const addWithLog = withLog(add)
addWithLog(1, 2)

6. 代理模式 (Proxy)

一句话定义

用一个“代理对象”站在真实对象前面,帮你做权限、缓存、延迟加载等控制。

生活类比
  • 明星不会自己接每一个活动电话,都是经纪人先接,再决定要不要转给明星。
  • 小区门口保安:不是任何人都能进,要先经过保安这一层判断。
解决什么问题
  • 访问某个对象前,需要统一加一层逻辑(鉴权/限流/缓存等)。
  • 不想在真实对象里到处写重复的前置检查。
JS 示例:缓存代理
// 原始的“慢函数”,比如从数据库/远程服务器获取数据
async function fetchUserFromServer(id) {
  console.log('真正去服务器查:', id)
  // 模拟网络延迟
  await new Promise(r => setTimeout(r, 500))
  return { id, name: '用户' + id }
}

// 代理:加一层缓存
function createUserFetcherWithCache() {
  const cache = new Map()

  return async function getUser(id) {
    if (cache.has(id)) {
      console.log('从缓存读取:', id)
      return cache.get(id)
    }
    const user = await fetchUserFromServer(id)
    cache.set(id, user)
    return user
  }
}

const getUser = createUserFetcherWithCache()

7. 外观模式 (Facade)

一句话定义

对外提供一个非常简单的“门面函数”,内部帮你处理一堆复杂的子步骤。

生活类比
  • 体检中心“一条龙服务”:你只要在前台说“我要体检套餐 A”,后面排队、抽血、拍片都有人帮你安排。
  • 旅行社的“跟团游”:你只管交钱集合,机票、酒店、门票都由旅行社帮你处理。
解决什么问题
  • 系统内部流程很复杂,不想每个地方都自己写一遍所有细节。
  • 对外暴露太多函数让人头大,希望只有一个入口。
JS 示例:下单外观
function chooseGoods() {
  console.log('1. 选择商品')
}

function createOrder() {
  console.log('2. 生成订单')
}

function pay() {
  console.log('3. 支付')
}

function sendGoods() {
  console.log('4. 发货')
}

// 外观函数:对外只暴露 buy
function buy() {
  chooseGoods()
  createOrder()
  pay()
  sendGoods()
}

buy()

行为型模式

8. 观察者模式 (Observer)

一句话定义

一个对象(被观察者)维护一组“观察者”,当它自己状态变化时,会主动通知所有观察者。

生活类比
  • 你关注了一个博主(被观察者),他一发动态,你就会收到推送。
  • 公众号:你点了“关注”,以后每篇文章你都能收到通知。
解决什么问题
  • 一个对象变化,需要通知到多个“感兴趣的人”。
  • 不想让“被观察者”直接依赖具体的观察者是谁。
什么时候用
  • 简单事件机制。
  • 某个数据变化,多个 UI 需要联动更新。
JS 示例:自己实现一个最小观察者
class Subject {
  constructor() {
    this.observers = []
  }

  addObserver(fn) {
    this.observers.push(fn)
  }

  removeObserver(fn) {
    this.observers = this.observers.filter(o => o !== fn)
  }

  notify(data) {
    this.observers.forEach(fn => fn(data))
  }
}

const subject = new Subject()

function observerA(msg) {
  console.log('观察者A 收到:', msg)
}

subject.addObserver(observerA)
subject.addObserver((msg) => console.log('观察者B 收到:', msg))

subject.notify('有新消息了')
subject.removeObserver(observerA)
subject.notify('第二条消息')

9. 发布-订阅模式 (Pub-Sub)

一句话定义

有一个“中间的事件中心”,发布者向中心发布消息,订阅者向中心订阅消息,发布者和订阅者互相不知道对方是谁。

生活类比
  • 微信朋友圈:你发的是给“朋友圈服务器”,不是直接发给每个好友;好友是向微信服务器“订阅”你的动态。
  • 电台广播:电台发信号,谁在收听它并不关心。
和观察者的直觉区别
  • 观察者:被观察者维护观察者列表,自己一个个通知。
  • 发布-订阅:双方都只和“事件中心”打交道,彼此解耦更彻底。
JS 示例:简单事件总线
const EventBus = {
  events: {},

  on(event, handler) {
    (this.events[event] ||= []).push(handler)
  },

  off(event, handler) {
    if (!this.events[event]) return
    if (!handler) {
      delete this.events[event]
    } else {
      this.events[event] = this.events[event].filter(h => h !== handler)
    }
  },

  emit(event, data) {
    (this.events[event] || []).forEach(h => h(data))
  }
}

// 订阅
EventBus.on('order:created', (id) => console.log('客服知道有订单:', id))
EventBus.on('order:created', (id) => console.log('仓库知道有订单:', id))

// 发布
EventBus.emit('order:created', 123)

10. 策略模式 (Strategy)

一句话定义

把一组“可互相替换的算法/规则”封装成独立策略对象(或函数),用时根据条件选择其中一种。

生活类比
  • 出门:打车、地铁、骑车、走路,都是“从 A 到 B”的不同“策略”。
  • 算工资:按小时、按月、按项目提成,也都是“结算工资”的不同策略。
解决什么问题
  • 很多 if...elseswitch 判断类型,然后执行不同逻辑。
  • 想在不改调用代码的前提下,灵活增删算法。
JS 示例:打折策略
const discountStrategies = {
  none(price) {
    return price
  },
  vip(price) {
    return price * 0.8
  },
  coupon20(price) {
    return price - 20
  }
}

function calcPrice(price, type = 'none') {
  const strategy = discountStrategies[type] || discountStrategies.none
  return strategy(price)
}

console.log(calcPrice(100, 'vip'))      // 80
console.log(calcPrice(100, 'coupon20')) // 80

11. 命令模式 (Command)

一句话定义

把一次“操作”封装成一个对象,里面记录“怎么执行”和“怎么撤销”,方便排队、记录历史、撤销重做。

生活类比
  • 你桌上的便签本,每张便签写一个待办:可以执行、也可以划掉表示撤销。
  • Photoshop 里的“历史记录”:每一步操作都可以撤回、重做。
解决什么问题
  • 需要记录一系列操作,支持撤销和重做。
  • 需要把“请求的发送者”和“实际执行者”解耦。
JS 示例:可撤销的加减操作
class Command {
  constructor(doFn, undoFn) {
    this.doFn = doFn
    this.undoFn = undoFn
  }
  execute() { this.doFn() }
  undo() { this.undoFn() }
}

class CommandManager {
  constructor() {
    this.history = []
  }

  run(command) {
    command.execute()
    this.history.push(command)
  }

  undo() {
    const cmd = this.history.pop()
    cmd && cmd.undo()
  }
}

let value = 0
const manager = new CommandManager()

// 加 5 的命令
const add5 = new Command(
  () => { value += 5 },
  () => { value -= 5 }
)

// 减 3 的命令
const minus3 = new Command(
  () => { value -= 3 },
  () => { value += 3 }
)

manager.run(add5)
manager.run(minus3)
console.log(value) // 2
manager.undo()
console.log(value) // 5

12. 状态模式 (State)

一句话定义

把一个对象在不同状态下的行为拆成不同的“状态类”,通过切换状态对象来切换行为。

生活类比
  • 红绿灯:红灯只能“停”,绿灯只能“走”,黄灯“减速慢行”。行为取决于当前状态。
  • 订单:待支付、已支付、已发货、已完成,不同状态能做的操作不同。
解决什么问题
  • 充满 if (status === 'xxx') { ... } else if (...) { ... } 的庞大分支。
  • 状态越来越多时,修改变得非常痛苦。
JS 示例:简化版订单状态
class PendingState {
  constructor(order) { this.order = order }
  pay() {
    console.log('支付成功')
    this.order.setState(this.order.paidState)
  }
}

class PaidState {
  constructor(order) { this.order = order }
  ship() {
    console.log('已发货')
    this.order.setState(this.order.shippedState)
  }
}

class ShippedState {
  constructor(order) { this.order = order }
  ship() {
    console.log('已经发过货了,不能重复发货')
  }
}

class Order {
  constructor() {
    this.pendingState = new PendingState(this)
    this.paidState = new PaidState(this)
    this.shippedState = new ShippedState(this)
    this.state = this.pendingState
  }

  setState(state) {
    this.state = state
  }

  pay() { this.state.pay && this.state.pay() }
  ship() { this.state.ship && this.state.ship() }
}

const order = new Order()
order.pay()  // 支付成功
order.ship() // 已发货
order.ship() // 已经发过货了,不能重复发货

13. 中介者模式 (Mediator)

一句话定义

用一个“中介对象”来封装多个对象之间的交互逻辑,让这些对象不直接引用彼此。

生活类比
  • 房产中介:房东和租客都只跟中介打交道,不直接互相谈。
  • 群聊里的“群管理员”:有人申请入群、有人发广告,都由管理员协调处理。
解决什么问题
  • 多个对象彼此互相调用,关系变得像“蜘蛛网”一样复杂。
  • 需要集中管理一组对象之间的协作,而不是分散在各个对象内部。
JS 示例:聊天室中介者(简化版)
class ChatRoom {
  constructor() {
    this.users = {}
  }

  register(user) {
    this.users[user.name] = user
    user.chatRoom = this
  }

  send(message, from, toName) {
    if (toName) {
      const toUser = this.users[toName]
      toUser && toUser.receive(message, from)
    } else {
      // 群发
      Object.values(this.users).forEach(user => {
        if (user.name !== from.name) {
          user.receive(message, from)
        }
      })
    }
  }
}

class User {
  constructor(name) {
    this.name = name
    this.chatRoom = null
  }

  send(message, toName) {
    this.chatRoom && this.chatRoom.send(message, this, toName)
  }

  receive(message, from) {
    console.log(`${this.name} 收到来自 ${from.name} 的消息: ${message}`)
  }
}

const room = new ChatRoom()
const alice = new User('Alice')
const bob = new User('Bob')
room.register(alice)
room.register(bob)

alice.send('你好,Bob', 'Bob')
bob.send('大家好', null) // 群发

设计模式快速选择指南

场景/需求推荐模式
需要一个全局唯一对象(配置、日志、缓存等)单例模式
需要根据条件创建不同类型对象工厂模式
需要链式配置构造复杂对象建造者模式
老数据/老接口格式不统一,需要转换适配器模式
想给函数或对象“加功能”但不改原代码装饰器模式
访问对象前统一做权限/缓存/日志等代理模式
内部流程复杂,对外只想暴露一个简单入口外观模式
一个对象变化,要通知多个“监听方”观察者模式
需要一个全局事件中心做解耦通信发布-订阅模式
充满 if-else/switch 的“不同规则”策略模式
需要记录历史、支持撤销/重做命令模式
各种 status 分支太多,希望按状态拆分逻辑状态模式
多个对象之间错综复杂的交互中介者模式

学习建议

  1. 先记住生活类比,再看代码
    例如:

    • 单例 = “一个中心”;工厂 = “后厨”;建造者 = “点奶茶”;
    • 适配器 = “转换插头”;装饰器 = “给咖啡加料”;代理 = “经纪人”;
    • 外观 = “体检套餐”;观察者/发布订阅 = “关注+朋友圈”;
    • 策略 = “出行方式”;命令 = “待办便签”;状态 = “红绿灯”;中介者 = “房产中介”。
  2. 遇到真实场景再反推模式

    • 写 if-else 写到崩溃 → 想想是不是可以用“策略模式”。
    • 多个地方用到全局配置 → 想想是不是该做成“单例”。
    • 一个操作要支持撤销/重做 → 想想是不是用“命令模式”。
  3. 先会用,再搞懂名字
    实战里你可以先按“套路”写出来,之后再对照名字记忆:
    “哦,原来我之前写的这个,其实就是 XXX 模式。”