设计模式初探

前人踩过的坑,总结成的"套路"

🎯 学习目标

学完本章,你将能够:

  • 能够理解并实现单例模式、工厂模式、观察者模式等经典设计模式
  • 能够运用发布/订阅模式实现模块间解耦通信
  • 能够使用策略模式替换条件分支,提升代码可扩展性
  • 能够理解中间件模式(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: "张三" });
观察者 vs 发布/订阅的区别:观察者中发布者知道订阅者是谁;发布/订阅中两者完全解耦,通过事件总线中转。项目大了用发布/订阅更灵活。

策略模式

把算法封装成可互换的策略

当你有一堆 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]
易错点:不要忘记处理"非法转换"。比如已完成的订单不能再支付,必须抛出明确错误,否则状态机会被破坏。

中间件实战

综合练习:用中间件模式实现请求处理器

练习三(挑战)

任务:实现一个简易请求处理器,基础功能是记录请求日志和返回响应。用中间件扩展以下功能:

  1. 认证检查:检查 header 中是否有 token
  2. 请求计时:记录处理耗时(ms)
  3. 错误捕获:捕获处理中的异常并返回 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 加上类型系统,让代码更安全。

1 / X