设计模式初探
前人踩过的坑,总结成的"套路"
🎯 学习目标
学完本章,你将能够:
- 能够理解并实现单例模式、工厂模式、观察者模式等经典设计模式
- 能够运用发布/订阅模式实现模块间解耦通信
- 能够使用策略模式替换条件分支,提升代码可扩展性
- 能够理解中间件模式(Koa/Express)的洋葱模型原理
- 能够识别常见场景并选择合适的设计模式解决问题
你有没有想过
为什么项目越写越乱?为什么改一个功能牵连十个文件?为什么同样的逻辑到处复制粘贴?
这些问题不是你代码写得少,而是缺少设计模式的指导。设计模式是前人在大量实践中总结出来的、解决特定问题的通用方案。
用人话说:设计模式就是编程界的"菜谱"——遇到什么问题,翻开对应的那一页,照着做。
这一章介绍最实用的几种模式:单例、工厂、观察者、发布/订阅、策略、代理、装饰器。
Part 1: 创建型模式
关注"怎么创建对象"。单例保证唯一,工厂隐藏创建细节
数据库连接在整个应用中只需要一个,new 多了反而出问题——怎么保证?创建对象时要写一堆 if/else 判断类型——能不能让调用方不关心这些细节?创建型模式就是解决"怎么创建对象"的。这一节学两个最常用的:单例模式保证全局唯一,工厂模式把创建逻辑藏起来。
单例模式
一个类只允许创建一个实例
数据库连接、全局配置、日志管理器——这些东西在整个应用中只需要一个。单例模式确保不管 new 多少次,拿到的都是同一个对象。
class Database {
static #instance = null;
constructor() {
if (Database.#instance) return Database.#instance;
this.connected = true;
Database.#instance = this;
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
实现原理:用一个静态私有变量保存实例。第二次 new 时发现已经有实例了,直接返回它。
工厂模式
不直接 new,通过工厂函数创建对象
当你需要根据不同条件创建不同对象时,工厂模式让创建逻辑集中在一处,调用方不用关心具体实现。
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() { return `${this.name}: ${this.sound}`; }
}
function createAnimal(type) {
const map = {
dog: () => new Animal("Dog", "Woof"),
cat: () => new Animal("Cat", "Meow"),
duck: () => new Animal("Duck", "Quack"),
};
return (map[type] || map.dog)();
}
console.log(createAnimal("cat").speak()); // "Cat: Meow"
好处:如果新增一种动物,只需要在 map 里加一行,调用方代码完全不用改。
Part 2: 行为型模式
关注"对象之间怎么通信"
对象创建好了,但它们之间怎么"说话"?模块 A 直接调用模块 B,改 B 的时候 A 也得改——代码耦合得像一团乱麻。行为型模式解决的就是"对象之间怎么通信"。观察者模式让一个变化通知所有人,策略模式把一堆 if/else 变成可插拔的方案。学完你会发现,日常开发中的很多痛点其实都有现成解法。
观察者模式
一个对象变化,所有订阅者自动收到通知
观察者模式就像微信群:群主发一条消息,所有群成员都能收到。成员可以随时加入或退出。
class EventEmitter {
#listeners = {};
on(event, fn) {
(this.#listeners[event] ??= []).push(fn);
return () => this.off(event, fn); // 返回取消订阅函数
}
off(event, fn) {
this.#listeners[event] =
this.#listeners[event]?.filter(f => f !== fn) ?? [];
}
emit(event, ...args) {
this.#listeners[event]?.forEach(fn => fn(...args));
}
}
const emitter = new EventEmitter();
const unsub = emitter.on("data", data => console.log("收到:", data));
emitter.emit("data", { id: 1 }); // "收到: {id: 1}"
unsub(); // 取消订阅
发布/订阅模式
和观察者的区别:中间多了一个"调度中心"
观察者模式中,发布者直接通知订阅者。发布/订阅模式中,发布者和订阅者互不认识,通过一个"事件总线"通信。
class PubSub {
#topics = new Map();
subscribe(topic, callback) {
if (!this.#topics.has(topic))
this.#topics.set(topic, new Set());
this.#topics.get(topic).add(callback);
return () => this.#topics.get(topic)?.delete(callback);
}
publish(topic, data) {
this.#topics.get(topic)?.forEach(cb => cb(data));
}
}
const bus = new PubSub();
bus.subscribe("user:login", user => console.log(`${user.name} 登录`));
bus.publish("user:login", { name: "张三" });
策略模式
把算法封装成可互换的策略
当你有一堆 if/else 或 switch 来选择不同算法时,策略模式能让代码更清晰、更易扩展。
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
function calculate(strategy, a, b) {
return strategies[strategy]?.(a, b) ?? NaN;
}
console.log(calculate("add", 2, 3)); // 5
console.log(calculate("multiply", 2, 3)); // 6
新增策略只需在对象里加一个键值对,不用改 calculate 函数。这比写一堆 if/else 好维护得多。
Part 3: 结构型模式
关注"怎么组合对象"
对象有了,通信方式也有了。但有时候你想在不改原代码的情况下给它"加点料"——比如给函数加日志、给对象加缓存。结构型模式关注的就是"怎么组合和包装对象"。代理模式在对象外面套一层拦截器,装饰器模式给函数动态加功能,依赖注入让代码不再写死依赖关系。
代理模式
Proxy:给对象加一层"拦截器"
代理模式就像明星的经纪人:粉丝不能直接联系明星,得通过经纪人。经纪人可以在中间做过滤、记录、缓存。
const handler = {
get(target, prop) {
console.log(`访问: ${String(prop)}`);
return target[prop];
},
set(target, prop, value) {
console.log(`设置: ${String(prop)} = ${value}`);
target[prop] = value;
return true;
}
};
const proxy = new Proxy({ name: "张三" }, handler);
proxy.name; // 打印 "访问: name"
proxy.age = 25; // 打印 "设置: age = 25"
Vue 3 的响应式系统就是基于 Proxy 实现的——拦截 get/set,自动追踪依赖和触发更新。
装饰器模式
不修改原函数,动态给它"加功能"
function withLogging(fn) {
return function(...args) {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn.apply(this, args);
console.log(`返回:`, result);
return result;
};
}
const add = withLogging((a, b) => a + b);
add(1, 2);
// 调用 add,参数: [1, 2]
// 返回: 3
装饰器的精髓:原函数一行不改,但在它外面"包了一层"新功能。日志、缓存、权限检查都可以用这种方式添加。
依赖注入
不自己创建依赖,由外部传入
// UserService 不自己 new Repository,而是从外部注入
class UserService {
constructor(repository) {
this.repo = repository; // 注入依赖
}
async getUser(id) {
return this.repo.findById(id);
}
}
// 在应用启动时决定用哪个实现
const service = new UserService(new PostgresUserRepo());
// 测试时换成 mock
const testService = new UserService(new MockUserRepo());
依赖注入让代码松耦合:UserService 不关心数据来自 PostgreSQL 还是内存,它只依赖接口。测试时可以轻松替换为 mock。
Part 4: 实战对照
设计模式不是理论课,而是解决实际问题的工具
学了这么多模式,到底什么时候该用哪个?这一节不做新概念的讲解,而是把日常开发中的真实痛点列出来,对号入座找到对应的模式。同时会补充中间件模式、状态模式两个实战常用的模式,最后帮你避开"为了用模式而用模式"的常见误区。
痛点与模式对照
对号入座,看看你的项目需要哪个模式
| 痛点 | 对应模式 | 解决方式 |
|---|---|---|
| 全局状态散乱 | 单例 + 观察者 | 统一状态管理 |
| 回调嵌套 | Promise/async-await | 扁平化异步流程 |
| 硬编码依赖 | 工厂 + 依赖注入 | 解耦创建和使用 |
| 重复代码 | 策略 / 模板方法 | 封装可复用逻辑 |
| 模块紧耦合 | 发布/订阅 | 通过事件总线通信 |
| 需要动态加功能 | 装饰器 | 包装原函数,不修改代码 |
常见误区
误区一:到处用单例
问题:单例是全局状态,测试时很难 mock,容易导致隐性依赖。
正确做法:只在真正需要全局唯一的场景使用(如数据库连接、日志器)。
误区二:过度使用观察者
问题:事件太多时,数据流向不清晰,调试时不知道是谁触发的。
正确做法:简单的一对一通信用直接调用,只有需要解耦时才用观察者。
误区三:设计模式能解决所有问题
问题:过度设计,一个简单功能套了三层模式,反而更难维护。
正确做法:先写出能跑的代码,发现痛点了再引入对应的模式重构。
中间件模式
Express / Koa 的核心:函数组合 + next() 调用链
中间件模式的本质是把一个个独立的功能串成一条处理链。每个中间件处理完后调用 next() 把控制权交给下一个,就像流水线上的工人各司其职。
function compose(middlewares) {
return function (ctx) {
let index = -1;
function dispatch(i) {
if (i <= index) throw new Error("next() called multiple times");
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
}
return dispatch(0);
};
}
// 使用示例
const app = compose([
async (ctx, next) => {
console.log("1. 请求开始");
await next();
console.log("4. 请求结束");
},
async (ctx, next) => {
console.log("2. 处理业务");
ctx.body = "Hello";
await next();
console.log("3. 业务完成");
},
]);
app({}).then(() => console.log("done"));
// 输出: 1 → 2 → 3 → 4 → done
Express 用类似机制处理请求;Koa 的洋葱模型更进一步——next() 之后的代码在响应返回时执行,适合写日志、计时等"后置"逻辑。
await next() 会导致后续中间件不执行,或执行顺序错乱。在 async 中间件里一定要 await。
状态模式
用对象映射替代 if/else 状态判断
当一个对象的行为取决于它的状态,而且状态会来回切换时,状态模式比写一堆 if/else 清晰得多。每个状态定义自己允许的转换,就像订单在各个阶段只能做特定操作。
const transitions = {
pending: { pay: "paid" },
paid: { ship: "shipped" },
shipped: { confirm: "completed" },
completed: {},
};
class Order {
#state = "pending";
get state() { return this.#state; }
handle(action) {
const next = transitions[this.#state]?.[action];
if (!next) {
throw new Error(
`状态 [${this.#state}] 不允许操作 [${action}]`
);
}
console.log(`${this.#state} --${action}--> ${next}`);
this.#state = next;
}
}
const order = new Order();
order.handle("pay"); // pending --pay--> paid
order.handle("ship"); // paid --ship--> shipped
order.handle("confirm"); // shipped --confirm--> completed
order.handle("pay"); // Error: 状态 [completed] 不允许操作 [pay]
中间件实战
综合练习:用中间件模式实现请求处理器
练习三(挑战)
任务:实现一个简易请求处理器,基础功能是记录请求日志和返回响应。用中间件扩展以下功能:
- 认证检查:检查 header 中是否有 token
- 请求计时:记录处理耗时(ms)
- 错误捕获:捕获处理中的异常并返回 500
要求中间件顺序可配置。
// 提示:
// 1. 用 compose 组合中间件
// 2. ctx 结构: { headers, body, statusCode }
// 3. 错误捕获中间件要放在最外层(第一个)
// 你的代码:
参考答案
function compose(middlewares) {
return (ctx) => {
let index = -1;
const dispatch = (i) => {
if (i <= index) throw new Error("next() called multiple times");
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
};
return dispatch(0);
};
}
// 错误捕获中间件(放最外层)
const errorCatch = async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.statusCode = 500;
ctx.body = { error: err.message };
console.error("捕获错误:", err.message);
}
};
// 认证中间件
const auth = async (ctx, next) => {
if (!ctx.headers.token) {
throw new Error("未提供认证 token");
}
console.log("认证通过");
await next();
};
// 计时中间件
const timer = async (ctx, next) => {
const start = Date.now();
await next();
console.log(`耗时: ${Date.now() - start}ms`);
};
// 基础处理器
const handler = async (ctx, next) => {
console.log("处理请求:", ctx.body);
ctx.statusCode = 200;
ctx.body = { msg: "success" };
await next();
};
// 中间件顺序可配置
const app = compose([errorCatch, auth, timer, handler]);
// 测试
app({ headers: { token: "abc" }, body: "test" });
// 认证通过 → 处理请求 → 耗时: Xms
app({ headers: {}, body: "test" });
// 捕获错误: 未提供认证 token
试一试
练习一
任务:用观察者模式实现一个简单的消息通知系统。支持 subscribe、unsubscribe、notify 三个方法。
// 提示:维护一个回调数组
// 你的代码:
参考答案
class Notifier {
#subscribers = [];
subscribe(fn) {
this.#subscribers.push(fn);
return () => {
this.#subscribers = this.#subscribers.filter(f => f !== fn);
};
}
notify(message) {
this.#subscribers.forEach(fn => fn(message));
}
}
const notifier = new Notifier();
const unsub = notifier.subscribe(msg => console.log(`收到: ${msg}`));
notifier.notify("系统维护通知"); // 收到: 系统维护通知
unsub();
练习二
任务:用策略模式实现一个简单的计算器,支持加减乘除。新增运算方式时不需要修改 calculate 函数。
// 提示:用对象存策略函数
// 你的代码:
参考答案
const strategies = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => b !== 0 ? a / b : NaN,
};
function calculate(op, a, b) {
if (!strategies[op]) throw new Error(`未知运算: ${op}`);
return strategies[op](a, b);
}
console.log(calculate("add", 10, 5)); // 15
console.log(calculate("divide", 10, 2)); // 5
// 新增运算:只需加一行,calculate 不用改
strategies.modulo = (a, b) => a % b;
📝 要点总结
本章核心知识点
创建型模式
单例保证唯一实例,工厂封装创建逻辑
行为型模式
观察者监听变化,策略封装算法,状态管理状态转换
结构型模式
代理控制访问,装饰器动态扩展功能
中间件模式
洋葱模型,compose 函数串联处理流程
设计原则
开闭原则、单一职责、依赖倒置——模式背后的思想
下一章
设计模式为你提供了解决常见架构问题的思路。下一章学习TypeScript——给 JavaScript 加上类型系统,让代码更安全。