在当今快速更迭的互联网业务中,后端的复杂性常常随着功能的堆砌而呈指数级增长。传统的MVC(模型-视图-控制器)架构在面对中大型复杂业务时,往往容易导致“胖模型”或“臃肿的业务逻辑层”,使得代码的可测试性和可维护性急剧下降。
为了应对这一挑战,**领域驱动设计(DDD, Domain-Driven Design)与干净架构(Clean Architecture)**的结合方案逐渐成为了构建高质量 Node.js / TypeScript 后端应用的首选。本文将深度剖析如何在 Node.js 环境下,利用 TypeScript 强类型特性,落地一套高内聚、低耦合的干净架构。
为什么选择干净架构与DDD?
干净架构由著名的“Uncle Bob”(Robert C. Martin)提出,其核心思想是关注点分离(Separation of Concerns)。整个架构被设计为一个同心圆,其核心规则是依赖规则(Dependency Rule):内层圆不能知道外层圆的任何信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ┌────────────────────────────────────────┐ │ 外层:外设与接口 │ │ (Express/NestJS, MongoDB/Postgres) │ │ ┌──────────────────────────┐ │ │ │ 中层:接口适配器 │ │ │ │ (Controllers, Presenters)│ │ │ │ ┌─────────────────┐ │ │ │ │ │ 内层:业务用例 │ │ │ │ │ │ (Use Cases) │ │ │ │ │ │ ┌────────┐ │ │ │ │ │ │ │核心领域│ │ │ │ │ │ │ │(Domain)│ │ │ │ │ │ │ └────────┘ │ │ │ │ │ └─────────────────┘ │ │ │ └──────────────────────────┘ │ └────────────────────────────────────────┘
|
- 核心领域层(Domain Layer):定义核心的业务实体(Entities)、值对象(Value Objects)和领域服务,这是最纯粹的业务逻辑,不依赖任何第三方库。
- 应用层/业务用例(Use Cases Layer):组织业务流程,将核心领域对象串联起来,完成特定的用户故事或功能。
- 接口适配器层(Interface Adapters Layer):将外层的控制器、持久化框架等具体技术转换为内层用例所期待的数据格式。
- 基础设施层(Infrastructure Layer):包含数据库连接、Web 框架、文件存储、第三方API等具体技术实现。
用 TypeScript 实战落地干净架构
接下来,我们以一个电商系统中的“用户注册与账户创建”用例为例,演示如何用 TypeScript 进行多层代码实现。
1. 核心领域层 (Domain)
首先是我们的核心实体 User 和值对象 Email。值对象具有不变性,并包含自我验证逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| export class Email { private readonly value: string;
private constructor(value: string) { this.value = value; }
public static create(email: string): Email { if (!email || !email.includes('@')) { throw new Error('无效的邮箱格式'); } return new Email(email.toLowerCase().trim()); }
public getValue(): string { return this.value; } }
import { Email } from '../value-objects/Email';
export interface UserProps { id: string; name: string; email: Email; createdAt: Date; }
export class User { private props: UserProps;
constructor(props: UserProps) { this.props = props; }
public get id(): string { return this.props.id; } public get name(): string { return this.props.name; } public get email(): Email { return this.props.email; } public get createdAt(): Date { return this.props.createdAt; }
public changeName(newName: string): void { if (!newName || newName.length < 2) { throw new Error('名字长度不能少于2个字符'); } this.props.name = newName; } }
|
2. 应用层仓储定义与用例 (Use Cases)
应用层定义了对外层具体实现的依赖契约(即接口,如 Repository 接口),并通过控制反转(IoC)将其注入到用例中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import { User } from '../../domain/entities/User';
export interface UserRepository { exists(email: string): Promise<boolean>; save(user: User): Promise<void>; findById(id: string): Promise<User | null>; }
import { User } from '../domain/entities/User'; import { Email } from '../domain/value-objects/Email'; import { UserRepository } from './ports/UserRepository';
export interface RegisterUserDTO { name: string; email: string; }
export class RegisterUserUseCase { private userRepo: UserRepository;
constructor(userRepo: UserRepository) { this.userRepo = userRepo; }
public async execute(request: RegisterUserDTO): Promise<User> { const emailOrError = Email.create(request.email); const userAlreadyExists = await this.userRepo.exists(emailOrError.getValue()); if (userAlreadyExists) { throw new Error('该邮箱已被注册'); }
const newUser = new User({ id: Math.random().toString(36).substr(2, 9), name: request.name, email: emailOrError, createdAt: new Date() });
await this.userRepo.save(newUser);
return newUser; } }
|
3. 基础设施层:具体技术实现 (Infrastructure)
在基础设施层中,我们使用真实的数据库(例如以 MongoDB/Mongoose 为例)实现仓储接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { UserRepository } from '../../use-cases/ports/UserRepository'; import { User } from '../../domain/entities/User'; import { Email } from '../../domain/value-objects/Email'; import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({ userId: String, name: String, email: String, createdAt: Date });
const UserModel = mongoose.model('User', UserSchema);
export class MongooseUserRepository implements UserRepository { public async exists(email: string): Promise<boolean> { const count = await UserModel.countDocuments({ email }); return count > 0; }
public async save(user: User): Promise<void> { const userDoc = new UserModel({ userId: user.id, name: user.name, email: user.email.getValue(), createdAt: user.createdAt }); await userDoc.save(); }
public async findById(id: string): Promise<User | null> { const doc = await UserModel.findOne({ userId: id }); if (!doc) return null; return new User({ id: doc.userId!, name: doc.name!, email: Email.create(doc.email!), createdAt: doc.createdAt! }); } }
|
干净架构的核心价值优势
- 框架无关(Framework Independence):核心的业务逻辑不依赖于 Express、Koa、NestJS 或任何特定的 Web 框架。如果我们未来想从 Express 切换到 Fastify 甚至 AWS Lambda 无服务器环境,只需更改最外层的适配器层,核心的领域逻辑和用例代码完全不需要变动。
- 易于单元测试(Testability):由于应用用例层和核心领域层都是不依赖具体技术的纯 TypeScript 代码,我们可以使用 Mock 轻易地编写极其快速的单元测试,甚至在数据库、外部API尚未准备妥当之前,就能对核心业务流程进行自动化覆盖验证。
- 数据库无关(Database Independence):应用层只依赖
UserRepository 抽象接口。底层数据库是从 SQL(PostgreSQL、MySQL)迁移到 NoSQL(MongoDB、DynamoDB),对内层用例完全透明。
总结
干净架构在初期落地时需要编写较多的目录结构和接口契约,但这是一种**“先苦后甜”**的投资。当项目逐渐变大、业务错综复杂、依赖的外部库需要升级或重构时,这套高度解耦的系统将展现出无与伦比的安全感与稳定性。在面对高速发展的业务时,保持核心领域逻辑的纯粹与干净,才是延长后端系统生命周期的唯一法宝。