设计原则与模式总览

设计模式是针对特定场景下反复出现的问题的通用解决方案,不是必须套用的模板。判断一个模式该不该用,只有一个标准:它是否降低了当前这段代码的变更成本。

一、SOLID 原则

原则含义一句话判据
SRP 单一职责一个模块只有一个引起它变化的原因描述这个函数要用"并且"吗?
OCP 开放封闭对扩展开放,对修改封闭加一个新类型,要不要改老代码?
LSP 里氏替换子类必须能替换父类而不破坏程序正确性子类有没有"抛异常"或"什么都不做"地覆盖父类方法?
ISP 接口隔离不强迫使用者依赖它用不到的接口实现这个接口时,有没有被迫写空方法?
DIP 依赖倒置高层模块不依赖低层模块,两者都依赖抽象业务代码里有没有直接 new 一个具体实现?

单一职责(SRP)

// ❌ 一个函数做了取数、转换、渲染三件事,任何一处需求变动都要改它
async function renderUserList(el) {
    const res = await fetch('/api/users');
    const users = (await res.json()).data.map(u => ({ ...u, name: u.first + u.last }));
    el.innerHTML = users.map(u => `<li>${u.name}</li>`).join('');
}

// ✅ 拆开后,换接口只改 fetchUsers,换 UI 只改 renderList
const fetchUsers = () => fetch('/api/users').then(r => r.json());
const toViewModel = data => data.map(u => ({ ...u, name: u.first + u.last }));
const renderList = (el, items) => { el.innerHTML = items.map(i => `<li>${i.name}</li>`).join(''); };

SRP 不是"函数越短越好"

把一个 30 行的连贯逻辑硬拆成 6 个只被调用一次的 5 行函数,只会让阅读时不停跳转。判断依据是"变化的原因",不是行数。

开放封闭(OCP)

这是绝大多数设计模式想解决的核心问题。识别信号:每加一个新类型,就要回去改一个 if / else 或 switch。

// ❌ 每增加一种支付方式都要改这个函数
function pay(type, amount) {
    if (type === 'alipay') { /* ... */ }
    else if (type === 'wechat') { /* ... */ }
    else if (type === 'card') { /* ... */ }
}

// ✅ 新增支付方式只需注册,不动 pay 本身 —— 这就是策略模式
const payments = new Map();
const registerPayment = (type, handler) => payments.set(type, handler);
function pay(type, amount) {
    const handler = payments.get(type);
    if (!handler) throw new Error(`未知支付方式: ${type}`);
    return handler(amount);
}

依赖倒置(DIP)

// ❌ 业务逻辑直接依赖具体实现,无法替换、无法测试
class OrderService {
    constructor() { this.db = new MySQLClient(); }
}
// ✅ 依赖注入:依赖的是"能存储"这个抽象
class OrderService {
    constructor(storage) { this.storage = storage; }
}
new OrderService(new MySQLClient());     // 生产
new OrderService(new InMemoryStorage()); // 测试

二、其他重要原则

  • 迪米特法则(最少知识原则):一个对象应尽可能少地了解其他对象。信号是链式访问 a.b.c.d.doSomething() —— 中间任何一环变了你都得改。
  • 组合优于继承:继承是编译期确定的强耦合,且会因为多层继承产生"脆弱基类"问题。JS 中优先用组合、mixin、高阶函数。
  • YAGNI(You Aren't Gonna Need It):不要为想象中的需求提前抽象。过早抽象比重复代码更难拆解。
  • KISS / DRY:DRY 消除的是"知识"的重复,不是"代码长得像"。两段碰巧相同但会朝不同方向演化的代码,强行合并反而制造耦合。

过度设计是比不设计更常见的问题

前端项目里出现"XxxFactoryBuilderStrategyManager"这类命名时,通常意味着抽象层数超过了问题本身的复杂度。先用最直白的写法写出来,等到第三次出现重复或第二次被迫改动同一处开关时,再考虑引入模式。

三、23 种模式分类

类型模式前端使用频度
创建型
怎么创建对象
单例⭐⭐⭐
工厂方法 / 抽象工厂 / 简单工厂⭐⭐⭐
建造者⭐⭐
原型⭐(JS 语言本身就是原型继承)
结构型
怎么组合对象
代理⭐⭐⭐
装饰器⭐⭐⭐
适配器⭐⭐⭐
外观⭐⭐⭐
组合⭐⭐
享元⭐⭐
桥接⭐
行为型
怎么分配职责
观察者 / 发布订阅⭐⭐⭐
策略⭐⭐⭐
迭代器⭐⭐⭐
命令⭐⭐
职责链⭐⭐⭐
中介者⭐⭐
模板方法⭐⭐
状态⭐⭐
备忘录 / 访问者 / 解释器⭐

四、JavaScript 让很多模式"消失"了

GoF 的 23 种模式诞生于 C++/Java 语境。JS 有头等函数、闭包、原型、动态类型,因此不少模式退化成了语言特性:

模式在 JS 中的形态
策略一个存放函数的对象 / Map,不需要定义策略接口和一堆类
命令一个函数(或闭包),不需要 Command 类
工厂一个返回对象字面量的普通函数
单例一个 ES Module —— 模块天然只求值一次
迭代器Symbol.iterator + for...of + 生成器
装饰器高阶函数
模板方法高阶函数 + 回调
原型语言内置

学习模式的正确姿势

不要背 UML 类图,要记住每个模式解决的具体痛点和识别信号:

  • 到处是 if/else 分支 → 策略 / 状态 / 职责链
  • 新增类型要改老代码 → 工厂 / 策略
  • 对象之间引用成网 → 中介者 / 发布订阅
  • 要在不改原对象的前提下加功能 → 装饰器 / 代理
  • 要控制对某个对象的访问 → 代理
  • 第三方接口和你要的形状不一致 → 适配器
  • 调用方要记住一长串步骤 → 外观

五、高阶函数:JS 中最常用的"元模式"

上面大半的模式在 JS 里都是靠高阶函数实现的。批量生成方法就是一个例子:

const Type = {};
['String', 'Number', 'Array', 'Function', 'Date', 'RegExp'].forEach(type => {
    Type[`is${type}`] = obj => Object.prototype.toString.call(obj) === `[object ${type}]`;
});

Type.isArray([]);      // true
Type.isString('abc');  // true

原来常见的 for 循环 + IIFE 写法是为了在 var 时代捕获循环变量。let 有块级作用域,forEach 的回调本身就是新作用域,都不再需要 IIFE。


下一步 👉 单例模式

上次更新:
贡献者: Joe