Node.js 设计模式与最佳实践:从 SOLID 到六边形架构

Node.js 开发中必备的设计模式与架构实践:SOLID 原则、23种经典设计模式的核心子集、模块与依赖注入、Event Emitter、中间件、Repository 模式、Clean Architecture 以及常见反模式与实战重构。

Node.js 开发随着项目复杂度增长,代码的组织方式直接决定了可维护性、可测试性与团队协作效率。当项目从一个简单的 API 脚本发展到包含数十个模块、多种外部依赖的大型应用时,如果没有统一的设计原则和模式约束,代码很快就会退化为难以理解和维护的面条代码。设计模式不是银弹,但它是前人经验的结晶,是经过大量工程实践验证的解决方案。

本文系统梳理 JavaScript/TypeScript 生态中常用的设计模式与架构原则,从 SOLID 基础到六边形架构,覆盖创建型、结构型、行为型三大类经典模式,以及 Node.js 特有的中间件、Event Emitter、Pub/Sub 等模式。通过大量 TypeScript 代码示例和实战重构案例,助你构建可长期演进、便于测试和团队协作的后端系统。文中所有代码示例基于 TypeScript 编写,兼顾类型安全与工程化实践。


1. SOLID 原则在 JavaScript/TypeScript 中的落地

SOLID 是面向对象设计的五大基本原则,最早由 Robert C. Martin 提出。在动态类型、鸭子类型的 JavaScript 中,这些原则同样不可或缺,甚至可以说更加重要,因为缺乏静态类型约束的 JavaScript 更容易出现职责混乱和紧耦合问题。TypeScript 的类型系统为这些原则提供了显式的约束手段,使编译器可以在编码阶段就发现潜在的架构问题。每一个 SOLID 原则都解决了一类特定的架构风险,它们共同构成了高质量软件设计的基石。

1.1 单一职责原则(SRP)

一个模块只负责一个领域的职责。违反 SRP 的典型信号是类的名称中出现了"和"字,例如"UserAndEmailService",或者一个方法内部混杂了数据校验、业务处理、数据持久化、外部通知等多种操作。当需求变更时,如果一个类因为多个不同的原因需要修改,就违背了单一职责原则。实践中,一个常见的衡量标准是:如果一个类或方法无法用一个简洁的句子描述其职责,那它很可能承担了过多职责。

// ❌ 违反 SRP:UserService 同时处理用户逻辑和邮件发送
class BadUserService {
    async createUser(data: UserInput) {
        const user = await this.db.insert(data);
        await this.sendWelcomeEmail(user.email);
        await this.logAudit('user.created', user.id);
        return user;
    }
}

// ✅ 职责分离
class UserService {
    constructor(
        private repo: IUserRepository,
        private emailService: IEmailService,
        private auditLogger: IAuditLogger
    ) {}

    async createUser(data: UserInput): Promise<User> {
        const user = await this.repo.save(data);
        await this.emailService.sendWelcome(user.email);
        await this.auditLogger.log('user.created', user.id);
        return user;
    }
}

1.2 开闭原则(OCP)

对扩展开放,对修改关闭。策略模式是实现 OCP 的主要手段。开闭原则的核心思想是:当系统需要新增功能时,应当通过新增代码来实现,而不是修改现有代码。这降低了引入回归缺陷的风险,也使得新增功能可以独立进行单元测试。在实际项目中,判断一个设计是否遵循 OCP,可以问自己一个问题:当需求变更时,我需要修改多少个已有的文件?理想情况下是零个。实现 OCP 的关键在于识别变化点,将变化的部分抽象为接口,让稳定的代码依赖这个抽象。

interface IPaymentProcessor {
    process(amount: number, currency: string): Promise<string>;
}

class StripeProcessor implements IPaymentProcessor {
    async process(amount: number, currency: string): Promise<string> {
        // Stripe 逻辑
        return `stripe_${amount}_${currency}`;
    }
}

class PayPalProcessor implements IPaymentProcessor {
    async process(amount: number, currency: string): Promise<string> {
        // PayPal 逻辑
        return `paypal_${amount}_${currency}`;
    }
}

// 新增支付方式时,只需新增实现类,不用修改 PaymentService
class PaymentService {
    constructor(private processor: IPaymentProcessor) {}

    async pay(amount: number, currency: string): Promise<string> {
        return this.processor.process(amount, currency);
    }
}

1.3 里氏替换原则(LSP)

子类必须能够替换其父类而不改变程序的正确性。里氏替换原则是继承的正确使用方式,它要求子类不能改变父类的行为契约。在 TypeScript 中,这意味着子类的实现必须满足接口声明的所有约束,不可以抛出父类方法未声明的异常,也不可以收窄返回类型或放宽参数类型。

interface IReadable {
    read(): string;
}

class FileReader implements IReadable {
    read(): string { return 'file content'; }
}

class HttpReader implements IReadable {
    read(): string { return 'http response'; }
}

function renderContent(reader: IReadable): void {
    console.log(reader.read());
}

renderContent(new FileReader());
renderContent(new HttpReader());  // 里氏替换

1.4 接口隔离原则(ISP)

客户端不应被迫依赖它们不使用的方法。

// ❌ 胖接口
interface IBigInterface {
    create(): void;
    read(): void;
    update(): void;
    delete(): void;
    sendEmail(): void;
    generateReport(): void;
}

// ✅ 拆分细粒度接口
interface ICrudRepository<T> {
    create(data: T): Promise<T>;
    read(id: string): Promise<T | null>;
    update(id: string, data: Partial<T>): Promise<T>;
    delete(id: string): Promise<void>;
}

interface IEmailService {
    send(to: string, subject: string, body: string): Promise<void>;
}

1.5 依赖倒置原则(DIP)

高层模块不应依赖低层模块,二者都应依赖抽象。这一原则是现代软件架构的基石。传统的三层架构中,业务逻辑层直接实例化数据库连接对象,导致任何数据库变动都会波及业务代码。依赖倒置通过引入抽象层,使高层模块(业务逻辑)仅依赖接口,而具体实现由低层模块(基础设施)提供。这种解耦使得单元测试可以使用内存中的 Mock 实现替代真实数据库,也使得系统可以在不修改核心业务代码的情况下切换底层技术栈。例如从关系型数据库迁移到文档数据库,或从 REST API 迁移到 gRPC。

// ❌ 直接依赖具体实现
class OrderService {
    private db = new PostgresConnection();  // 紧耦合
}

// ✅ 依赖抽象(接口)
class OrderService {
    constructor(private db: IDatabaseConnection) {}
}

DIP 是依赖注入和 Clean Architecture 的理论基础,也是实现可测试架构的前提条件。


2. 创建型模式:工厂、单例、建造者

创建型模式关注对象的创建逻辑,将对象的创建与使用解耦。在 JavaScript 中,对象的创建非常灵活,但也因此容易在代码各处散落 new 操作符,导致类之间的紧耦合。创建型模式通过集中化创建逻辑、限制实例数量或分步构建复杂对象,来提升代码的可维护性和灵活性。Node.js 项目中常见的场景包括数据库连接的创建、HTTP 客户端的初始化、配置对象的构建等,都可以通过创建型模式来优化。

2.1 工厂模式(Factory)

将对象创建逻辑封装,使调用者无需关心具体实现。工厂模式在实际项目中非常实用,特别是当创建的类需要根据运行时条件决定时。例如日志系统可能需要根据环境变量决定使用控制台日志还是文件日志,支付系统可能需要根据用户地区选择不同的支付通道。工厂模式将 “创建哪个对象” 的判断逻辑与 “如何使用对象” 的业务逻辑分离,使得代码更加清晰,也便于新增新的实现类型。

interface ILogger {
    log(message: string): void;
}

class ConsoleLogger implements ILogger {
    log(message: string): void {
        console.log(`[CONSOLE] ${message}`);
    }
}

class FileLogger implements ILogger {
    constructor(private path: string) {}
    log(message: string): void {
        // fs.appendFileSync(this.path, message + '\n');
    }
}

// 工厂函数
type LoggerType = 'console' | 'file';

class LoggerFactory {
    static create(type: LoggerType, options?: { path?: string }): ILogger {
        switch (type) {
            case 'console': return new ConsoleLogger();
            case 'file': return new FileLogger(options?.path ?? './app.log');
            default: throw new Error(`Unknown logger type: ${type}`);
        }
    }
}

const logger = LoggerFactory.create('console');
logger.log('Application started');

2.2 单例模式(Singleton)

确保全局只有一个实例,在 Node.js 中利用模块级别的 require 缓存机制可以天然实现单例效果。

// config.ts — Node.js 模块本身就是单例
class AppConfig {
    private static instance: AppConfig;
    public readonly env: string = process.env.NODE_ENV ?? 'development';
    public readonly port: number = Number(process.env.PORT ?? 3000);

    private constructor() {}  // 禁止外部实例化

    static getInstance(): AppConfig {
        if (!AppConfig.instance) {
            AppConfig.instance = new AppConfig();
        }
        return AppConfig.instance;
    }
}

export const config = AppConfig.getInstance();

注意:单例在测试中可能带来状态污染,通常应通过依赖注入注入配置对象,而不是全局访问。单例模式虽然解决了实例唯一性的问题,但也引入了全局访问点和测试困难等副作用。在单元测试中,如果多个测试用例共享同一个单例实例,前一个测试的状态可能影响后一个测试的结果。因此,现代 Node.js 项目中往往倾向于使用依赖注入容器来管理唯一实例,而不是传统的全局变量式单例。

2.3 建造者模式(Builder)

分步构造复杂对象,避免参数爆炸。

class HttpRequestBuilder {
    private method: string = 'GET';
    private url: string = '';
    private headers: Record<string, string> = {};
    private body?: object;

    setMethod(method: string): this {
        this.method = method;
        return this;
    }

    setUrl(url: string): this {
        this.url = url;
        return this;
    }

    addHeader(key: string, value: string): this {
        this.headers[key] = value;
        return this;
    }

    setBody(body: object): this {
        this.body = body;
        return this;
    }

    build(): RequestInit & { url: string } {
        return {
            url: this.url,
            method: this.method,
            headers: this.headers,
            ...(this.body ? { body: JSON.stringify(this.body) } : {})
        };
    }
}

const req = new HttpRequestBuilder()
    .setMethod('POST')
    .setUrl('/api/users')
    .addHeader('Content-Type', 'application/json')
    .addHeader('Authorization', 'Bearer xxx')
    .setBody({ name: 'Alice' })
    .build();

3. 结构型模式:适配器、装饰器、代理、外观

结构型模式关注如何将类或对象组合成更大的结构,使它们协同工作。在实际开发中,经常遇到新旧系统对接、功能动态增强、访问控制等需求,结构型模式为这些场景提供了优雅的解决方案。适配器模式让不兼容的接口能够协同工作,是系统集成的利器;装饰器模式在不改变原有类的情况下动态增加功能,常用于缓存、日志等横切关注点;代理模式用于控制对目标的访问,支持懒加载、权限校验等场景;外观模式则封装复杂的子系统,为客户端提供简洁的接口。这四种模式在 Node.js 中间件、API 网关、缓存层等场景中都有广泛应用。

3.1 适配器模式(Adapter)

将不兼容的接口转换为兼容的接口。

interface ICache {
    get(key: string): Promise<string | null>;
    set(key: string, value: string, ttl?: number): Promise<void>;
}

// Redis 原生客户端接口不同
class RedisAdapter implements ICache {
    constructor(private redisClient: RedisClientType) {}

    async get(key: string): Promise<string | null> {
        return this.redisClient.get(key);
    }

    async set(key: string, value: string, ttl?: number): Promise<void> {
        if (ttl) {
            await this.redisClient.setEx(key, ttl, value);
        } else {
            await this.redisClient.set(key, value);
        }
    }
}

// 业务逻辑只依赖 ICache,不关心底层是 Redis、Memcached 还是内存缓存

3.2 装饰器模式(Decorator)

动态增强对象功能,不修改原有类。

interface IRepository<T> {
    findById(id: string): Promise<T | null>;
}

// 基础实现
class UserRepository implements IRepository<User> {
    async findById(id: string): Promise<User | null> {
        // 数据库查询
        return { id, name: 'Alice' };
    }
}

// 装饰器:添加缓存层
class CachedUserRepository implements IRepository<User> {
    constructor(
        private repo: IRepository<User>,
        private cache: ICache
    ) {}

    async findById(id: string): Promise<User | null> {
        const cached = await this.cache.get(`user:${id}`);
        if (cached) return JSON.parse(cached);

        const user = await this.repo.findById(id);
        if (user) {
            await this.cache.set(`user:${id}`, JSON.stringify(user), 300);
        }
        return user;
    }
}

const baseRepo = new UserRepository();
const cachedRepo = new CachedUserRepository(baseRepo, redisAdapter);

3.3 代理模式(Proxy)

控制对目标对象的访问,可用于懒加载、权限校验、日志等。

interface IUserService {
    getUser(id: string): Promise<User>;
}

class UserServiceProxy implements IUserService {
    constructor(
        private service: IUserService,
        private logger: ILogger
    ) {}

    async getUser(id: string): Promise<User> {
        this.logger.log(`[PROXY] getUser called with id=${id}`);
        const start = Date.now();
        const result = await this.service.getUser(id);
        this.logger.log(`[PROXY] getUser completed in ${Date.now() - start}ms`);
        return result;
    }
}

TypeScript 还提供了语言级别的 Proxy:

const handler: ProxyHandler<object> = {
    get(target, prop) {
        console.log(`Accessing ${String(prop)}`);
        return Reflect.get(target, prop);
    },
    set(target, prop, value) {
        console.log(`Setting ${String(prop)} = ${value}`);
        return Reflect.set(target, prop, value);
    }
};

3.4 外观模式(Facade)

为复杂子系统提供统一的高层接口。

class DatabaseFacade {
    constructor(
        private userRepo: IUserRepository,
        private orderRepo: IOrderRepository,
        private notificationService: INotificationService
    ) {}

    async createOrderWithUser(userData: UserInput, orderData: OrderInput): Promise<void> {
        const user = await this.userRepo.save(userData);
        const order = await this.orderRepo.create({ ...orderData, userId: user.id });
        await this.notificationService.notify(user.email, `Order ${order.id} created`);
    }
}

// 客户端只需调用一个方法
await facade.createOrderWithUser(userData, orderData);

4. 行为型模式:观察者、策略、命令、职责链

行为型模式关注对象之间的通信和职责分配。与创建型模式处理对象如何被创建、结构型模式处理对象的组合方式不同,行为型模式处理的是对象之间如何交互和分配职责。在事件驱动的 Node.js 环境中,行为型模式有着天然的适用场景,能够让复杂的交互逻辑变得更加清晰和可维护。观察者模式与 Node.js 的 EventEmitter 一脉相承,策略模式让算法族可以灵活替换,命令模式支持操作的撤销和队列化,职责链模式则将请求沿着处理链传递。掌握这些模式可以帮助开发者写出更加松耦合、可扩展的事件处理逻辑。

4.1 观察者模式(Observer)

一对多的依赖关系,当被观察者状态改变时通知所有观察者。

interface IObserver<T> {
    update(data: T): void;
}

interface IObservable<T> {
    subscribe(observer: IObserver<T>): void;
    unsubscribe(observer: IObserver<T>): void;
    notify(data: T): void;
}

class OrderObservable implements IObservable<Order> {
    private observers: IObserver<Order>[] = [];

    subscribe(observer: IObserver<Order>): void {
        this.observers.push(observer);
    }

    unsubscribe(observer: IObserver<Order>): void {
        this.observers = this.observers.filter(o => o !== observer);
    }

    notify(data: Order): void {
        this.observers.forEach(o => o.update(data));
    }
}

class InventoryObserver implements IObserver<Order> {
    update(order: Order): void {
        console.log(`[Inventory] Deducting stock for product ${order.productId}`);
    }
}

class AnalyticsObserver implements IObserver<Order> {
    update(order: Order): void {
        console.log(`[Analytics] Recording order ${order.id}`);
    }
}

4.2 策略模式(Strategy)

封装算法族,使其可以互相替换。

interface IDiscountStrategy {
    calculateDiscount(amount: number): number;
}

class NoDiscountStrategy implements IDiscountStrategy {
    calculateDiscount(amount: number): number { return 0; }
}

class PercentageDiscountStrategy implements IDiscountStrategy {
    constructor(private percent: number) {}
    calculateDiscount(amount: number): number {
        return amount * (this.percent / 100);
    }
}

class FixedDiscountStrategy implements IDiscountStrategy {
    constructor(private discount: number) {}
    calculateDiscount(amount: number): number {
        return Math.min(this.discount, amount);
    }
}

class PriceCalculator {
    constructor(private strategy: IDiscountStrategy) {}

    setStrategy(strategy: IDiscountStrategy): void {
        this.strategy = strategy;
    }

    getFinalPrice(amount: number): number {
        return amount - this.strategy.calculateDiscount(amount);
    }
}

const calculator = new PriceCalculator(new PercentageDiscountStrategy(10));
console.log(calculator.getFinalPrice(100));  // 90

calculator.setStrategy(new FixedDiscountStrategy(30));
console.log(calculator.getFinalPrice(100));  // 70

4.3 命令模式(Command)

将请求封装为对象,支持撤销、重做、队列化。

interface ICommand {
    execute(): Promise<void>;
    undo(): Promise<void>;
}

class CreateUserCommand implements ICommand {
    constructor(
        private repo: IUserRepository,
        private data: UserInput,
        private createdId?: string
    ) {}

    async execute(): Promise<void> {
        const user = await this.repo.save(this.data);
        this.createdId = user.id;
    }

    async undo(): Promise<void> {
        if (this.createdId) {
            await this.repo.delete(this.createdId);
        }
    }
}

class CommandInvoker {
    private history: ICommand[] = [];

    async execute(cmd: ICommand): Promise<void> {
        await cmd.execute();
        this.history.push(cmd);
    }

    async undo(): Promise<void> {
        const cmd = this.history.pop();
        if (cmd) await cmd.undo();
    }
}

4.4 职责链模式(Chain of Responsibility)

将请求沿着处理链传递,直到被处理。

interface IHandler {
    setNext(handler: IHandler): IHandler;
    handle(request: Request): Response | null;
}

abstract class BaseHandler implements IHandler {
    private nextHandler?: IHandler;

    setNext(handler: IHandler): IHandler {
        this.nextHandler = handler;
        return handler;
    }

    handle(request: Request): Response | null {
        if (this.nextHandler) {
            return this.nextHandler.handle(request);
        }
        return null;
    }
}

class AuthHandler extends BaseHandler {
    handle(request: Request): Response | null {
        if (!request.headers.authorization) {
            return { status: 401, body: 'Unauthorized' };
        }
        return super.handle(request);
    }
}

class RateLimitHandler extends BaseHandler {
    handle(request: Request): Response | null {
        if (this.isRateLimited(request.ip)) {
            return { status: 429, body: 'Too many requests' };
        }
        return super.handle(request);
    }

    private isRateLimited(ip: string): boolean {
        return false;  // 简化示例
    }
}

class RouteHandler extends BaseHandler {
    handle(request: Request): Response | null {
        // 实际路由处理
        return { status: 200, body: 'OK' };
    }
}

// 组装链条
const auth = new AuthHandler();
const rateLimit = new RateLimitHandler();
const route = new RouteHandler();
auth.setNext(rateLimit).setNext(route);

const result = auth.handle({ headers: { authorization: 'Bearer xxx' }, ip: '127.0.0.1' });

5. Node.js 模块模式与依赖注入

5.1 CommonJS 模块模式

Node.js 的模块系统本身就是一种封装模式,利用了闭包的特性实现了变量的作用域隔离。从 CommonJS 到 ES Module,模块系统一直是 Node.js 组织代码的核心机制。每个文件就是一个模块,导出的内容通过显式声明暴露,避免了全局命名空间污染。

// services/logger.service.ts
const pino = require('pino');

const logger = pino({ level: process.env.LOG_LEVEL ?? 'info' });

module.exports = { logger };

// consumer.ts
const { logger } = require('./services/logger.service');

5.2 ES Module 与依赖注入容器

现代 Node.js 项目推荐 ESM + 构造函数注入的组合:

// container.ts —— 手动 DI 容器
import { PrismaClient } from '@prisma/client';
import { UserRepository } from './infrastructure/db/user.repository';
import { UserService } from './application/user.service';
import { UserController } from './infrastructure/http/user.controller';

export function createContainer() {
    const prisma = new PrismaClient();

    // 基础设施层
    const userRepo = new UserRepository(prisma);

    // 应用层
    const userService = new UserService(userRepo);

    // 接口适配层
    const userController = new UserController(userService);

    return { userController, userService, userRepo };
}

NestJS 提供的 @Injectable()@Module() 装饰器本质上就是一个自动化 DI 容器。


6. Event Emitter 与 Pub/Sub

6.1 内置 EventEmitter

Node.js 的 events 模块是最核心的异步通信机制。

import { EventEmitter } from 'events';

class OrderService extends EventEmitter {
    async createOrder(data: OrderInput): Promise<Order> {
        const order = await this.persist(data);

        this.emit('order:created', order);

        return order;
    }
}

const service = new OrderService();

// 监听事件
service.on('order:created', (order: Order) => {
    console.log(`Order created: ${order.id}`);
});

// 异步监听
service.on('order:created', async (order: Order) => {
    await sendNotificationEmail(order.userEmail);
});

6.2 基于 Redis 的分布式 Pub/Sub

在微服务或集群模式下,需要使用消息中间件代替内存事件:

import { createClient } from 'redis';

const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();

await publisher.connect();
await subscriber.connect();

// 订阅
await subscriber.subscribe('order:created', (message) => {
    const order = JSON.parse(message);
    console.log(`[Distributed Pub/Sub] Order ${order.id} received`);
});

// 发布
await publisher.publish('order:created', JSON.stringify({ id: '123', total: 199 }));

7. 中间件模式(Express / Koa)

中间件是 Node.js Web 框架的核心设计模式——洋葱模型。每一个中间件都是一个函数,接收请求对象、响应对象和下一个中间件函数作为参数。Express 的中间件是线性结构,而 Koa 则实现了更加优雅的洋葱模型,支持在中间件前后分别执行逻辑,使得横切关注点(如日志记录、耗时统计)的代码更加简洁。

import express, { Request, Response, NextFunction } from 'express';

const app = express();

// 中间件本质:接收 (req, res, next) 的函数链
app.use((req: Request, res: Response, next: NextFunction) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
    next();  // 传递控制权给下一个中间件
});

// 认证中间件
function authMiddleware(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (!token) {
        res.status(401).json({ error: 'Unauthorized' });
        return;
    }
    (req as any).user = { id: '123', role: 'admin' };  // 挂载到请求上下文
    next();
}

// 错误处理中间件(4参数签名)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.error(err.stack);
    res.status(500).json({ error: err.message });
});

中间件执行顺序

Request → Middleware A (before) → Middleware B (before) → Route Handler
                                                              ↓
Response ← Middleware A (after) ← Middleware B (after) ← Response

这是 Koa 洋葱模型的核心:控制权先从外到内,再从内到外。


8. Repository 模式与数据访问层

Repository 模式将数据访问逻辑从业务逻辑中解耦,是实现 Clean Architecture 的关键。

// domain/repositories/user.repository.interface.ts
export interface IUserRepository {
    findById(id: string): Promise<User | null>;
    findByEmail(email: string): Promise<User | null>;
    findAll(options: { skip: number; take: number }): Promise<User[]>;
    save(user: User): Promise<User>;
    delete(id: string): Promise<void>;
}

// infrastructure/db/prisma-user.repository.ts
export class PrismaUserRepository implements IUserRepository {
    constructor(private prisma: PrismaClient) {}

    async findById(id: string): Promise<User | null> {
        const record = await this.prisma.user.findUnique({ where: { id } });
        return record ? this.toDomain(record) : null;
    }

    async save(user: User): Promise<User> {
        const record = await this.prisma.user.upsert({
            where: { id: user.id },
            update: { name: user.name, email: user.email },
            create: { id: user.id, name: user.name, email: user.email }
        });
        return this.toDomain(record);
    }

    private toDomain(record: PrismaUser): User {
        return new User(record.id, record.name, record.email, record.createdAt);
    }
}

业务服务只依赖接口:

export class UserService {
    constructor(private userRepo: IUserRepository) {}

    async getUserProfile(id: string): Promise<UserProfile> {
        const user = await this.userRepo.findById(id);
        if (!user) throw new NotFoundError('User not found');
        return user.toProfile();
    }
}

9. Clean Architecture / 六边形架构

Clean Architecture(干净架构)强调依赖方向永远向内:外层依赖内层,内层不依赖外层。

9.1 分层结构

┌───────────────────────────────────────┐
│           Presentation Layer           │  ← Controllers, Routes, DTOs
│        (Infrastructure / UI)           │
├───────────────────────────────────────┤
│           Application Layer            │  ← Services, Use Cases, DTOs
│                                        │
├───────────────────────────────────────┤
│            Domain Layer                │  ← Entities, Value Objects,
│        (Core Business Logic)           │     Domain Events, Interfaces
└───────────────────────────────────────┘
           ↑ (外向内依赖)

9.2 端口与适配器(六边形架构)

// domain/ports/user.repository.port.ts —— "端口"(内层定义的接口)
export interface IUserRepositoryPort {
    findById(id: string): Promise<User | null>;
}

// infrastructure/adapters/prisma-user.adapter.ts —— "适配器"(外层实现)
export class PrismaUserAdapter implements IUserRepositoryPort {
    constructor(private prisma: PrismaClient) {}
    async findById(id: string): Promise<User | null> {
        const record = await this.prisma.user.findUnique({ where: { id } });
        return record ? new User(record.id, record.email) : null;
    }
}

// application/user.usecase.ts —— 用例只依赖端口
export class GetUserUseCase {
    constructor(private userRepo: IUserRepositoryPort) {}

    async execute(id: string): Promise<UserDto> {
        const user = await this.userRepo.findById(id);
        if (!user) throw new UserNotFoundError(id);
        return UserDto.fromDomain(user);
    }
}

9.3 依赖方向示意图

HTTP Controller ──→ Use Case ──→ Domain Entity
     ↑                              ↑
   Express                     纯 TypeScript
   (替换为 Fastify 无影响)      (替换数据库无影响)

10. 常见反模式与避免方法

10.1 回调地狱

// ❌ 回调地狱(Node.js 早期)
fs.readFile('a.txt', (err, a) => {
    fs.readFile('b.txt', (err, b) => {
        fs.readFile('c.txt', (err, c) => {
            console.log(a, b, c);
        });
    });
});

// ✅ 使用 async/await + Promise
const [a, b, c] = await Promise.all([
    fs.promises.readFile('a.txt', 'utf8'),
    fs.promises.readFile('b.txt', 'utf8'),
    fs.promises.readFile('c.txt', 'utf8')
]);

10.2 紧耦合

// ❌ 服务直接实例化依赖
class OrderService {
    private paymentService = new StripePaymentService();
    private notificationService = new EmailNotificationService();
}

// ✅ 通过构造函数注入解耦
class OrderService {
    constructor(
        private paymentService: IPaymentService,
        private notificationService: INotificationService
    ) {}
}

10.3 全局状态

// ❌ 全局变量污染
let currentUser: User | null = null;

function setUser(user: User): void {
    currentUser = user;
}

// ✅ 通过依赖注入或请求上下文传递
class RequestContext {
    constructor(public readonly user: User) {}
}

app.use((req, res, next) => {
    (req as any).context = new RequestContext(req.user);
    next();
});

10.4 上帝对象

// ❌ God Class:一个类做所有事情
class App {
    saveUser() {}
    processPayment() {}
    sendEmail() {}
    generateInvoice() {}
    connectDatabase() {}
}

// ✅ 按职责拆分
class UserService { ... }
class PaymentService { ... }
class EmailService { ... }
class InvoiceService { ... }
class DatabaseConnection { ... }

11. 实战:从面条代码到 Clean Architecture

11.1 阶段一:面条代码

// routes.ts — 所有逻辑堆在一起
app.post('/orders', async (req, res) => {
    const prisma = new PrismaClient();
    const { userId, items } = req.body;

    // 校验
    if (!userId || !items?.length) {
        return res.status(400).json({ error: 'Invalid input' });
    }

    // 查询用户
    const user = await prisma.user.findUnique({ where: { id: userId } });
    if (!user) return res.status(404).json({ error: 'User not found' });

    // 计算总价
    let total = 0;
    for (const item of items) {
        const product = await prisma.product.findUnique({ where: { id: item.productId } });
        if (!product) return res.status(404).json({ error: 'Product not found' });
        total += product.price * item.quantity;
    }

    // 创建订单
    const order = await prisma.order.create({
        data: { userId, total, status: 'pending' }
    });

    // 发送邮件
    await sendEmail(user.email, `Order ${order.id} created`);

    // 记录日志
    console.log(`Order created: ${order.id}`);

    res.status(201).json(order);
});

问题:

  • 路由处理函数承担了校验、业务逻辑、数据访问、通知
  • 无法独立测试
  • 无法更换数据库或通知方式
  • Prisma 直接污染了控制器层

11.2 阶段二:Clean Architecture 重构

// domain/entities/order.entity.ts
export class Order {
    constructor(
        public readonly id: string,
        public readonly userId: string,
        public readonly items: OrderItem[],
        public readonly total: number,
        public status: OrderStatus = 'pending'
    ) {}

    static create(userId: string, items: OrderItem[]): Order {
        const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
        return new Order(crypto.randomUUID(), userId, items, total);
    }
}

// domain/repositories/order.repository.interface.ts
export interface IOrderRepository {
    save(order: Order): Promise<Order>;
    findById(id: string): Promise<Order | null>;
}

// domain/services/pricing.service.ts
export class PricingService {
    calculateTotal(items: OrderItem[]): number {
        return items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
    }
}

// application/usecases/create-order.usecase.ts
export class CreateOrderUseCase {
    constructor(
        private orderRepo: IOrderRepository,
        private userRepo: IUserRepository,
        private productRepo: IProductRepository,
        private pricingService: PricingService,
        private notificationService: INotificationService
    ) {}

    async execute(input: CreateOrderInput): Promise<Order> {
        const user = await this.userRepo.findById(input.userId);
        if (!user) throw new UserNotFoundError(input.userId);

        const orderItems: OrderItem[] = [];
        for (const item of input.items) {
            const product = await this.productRepo.findById(item.productId);
            if (!product) throw new ProductNotFoundError(item.productId);
            if (product.stock < item.quantity) throw new InsufficientStockError(product.id);
            orderItems.push({ productId: product.id, unitPrice: product.price, quantity: item.quantity });
        }

        const total = this.pricingService.calculateTotal(orderItems);
        const order = new Order(crypto.randomUUID(), user.id, orderItems, total);

        await this.orderRepo.save(order);
        await this.notificationService.notify(user.email, `Order ${order.id} confirmed`);

        return order;
    }
}

// infrastructure/http/order.controller.ts
export class OrderController {
    constructor(private createOrderUseCase: CreateOrderUseCase) {}

    async create(req: Request, res: Response): Promise<void> {
        const input = CreateOrderDto.validate(req.body);  // 使用 DTO 校验
        const order = await this.createOrderUseCase.execute(input);
        res.status(201).json(OrderDto.fromDomain(order));
    }
}

// main.ts —— 组装依赖
const container = createContainer();
app.post('/orders', (req, res) => container.orderController.create(req, res));

11.3 重构后的收益

维度重构前重构后
可测试性需启动 Express + Prisma 才能测试纯业务逻辑可独立单元测试
可替换性更换数据库需修改路由文件只需替换 Repository 实现
可读性一个文件 50+ 行混杂逻辑每个类职责单一,边界清晰
可扩展性新增通知渠道需改路由新增实现 INotificationService 即可

总结

设计模式的价值不在于记忆模式名称,而在于理解其解决的问题适用边界。在 Node.js 开发中,建议遵循以下优先级:

  1. 先做好 SOLID 原则:这是所有模式的基础
  2. 善用模块系统封装:Node.js 的模块本身就是最强的边界
  3. 优先组合而非继承:TypeScript 的接口和对象组合比类继承更灵活
  4. Repository + 依赖注入:数据访问层分离是项目规模化后的必要投资
  5. Clean Architecture 分层:当项目超过一定复杂度后,明确的依赖方向能显著降低维护成本

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nodejs」更多文章

  1. Node.js ORM 深度对比:Prisma、TypeORM、Sequelize 与 Drizzle
  2. Node.js 高级测试策略:从单元测试到混沌工程的完整实践
  3. Node.js 可观测性实践:OpenTelemetry、监控与全链路追踪