在当今快速更迭的互联网业务中,后端的复杂性常常随着功能的堆砌而呈指数级增长。传统的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)│ │ │ │
│ │ │ └────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └──────────────────────────┘ │
└────────────────────────────────────────┘
  1. 核心领域层(Domain Layer):定义核心的业务实体(Entities)、值对象(Value Objects)和领域服务,这是最纯粹的业务逻辑,不依赖任何第三方库。
  2. 应用层/业务用例(Use Cases Layer):组织业务流程,将核心领域对象串联起来,完成特定的用户故事或功能。
  3. 接口适配器层(Interface Adapters Layer):将外层的控制器、持久化框架等具体技术转换为内层用例所期待的数据格式。
  4. 基础设施层(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
// domain/value-objects/Email.ts
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;
}
}

// domain/entities/User.ts
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
// use-cases/ports/UserRepository.ts
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>;
}

// use-cases/RegisterUserUseCase.ts
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);

// 1. 验证用户是否已存在
const userAlreadyExists = await this.userRepo.exists(emailOrError.getValue());
if (userAlreadyExists) {
throw new Error('该邮箱已被注册');
}

// 2. 创建领域对象
const newUser = new User({
id: Math.random().toString(36).substr(2, 9), // 简易ID生成
name: request.name,
email: emailOrError,
createdAt: new Date()
});

// 3. 持久化存储
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
// infrastructure/database/MongooseUserRepository.ts
import { UserRepository } from '../../use-cases/ports/UserRepository';
import { User } from '../../domain/entities/User';
import { Email } from '../../domain/value-objects/Email';
import mongoose from 'mongoose';

// 定义真实的 Schema
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!
});
}
}

干净架构的核心价值优势

  1. 框架无关(Framework Independence):核心的业务逻辑不依赖于 Express、Koa、NestJS 或任何特定的 Web 框架。如果我们未来想从 Express 切换到 Fastify 甚至 AWS Lambda 无服务器环境,只需更改最外层的适配器层,核心的领域逻辑和用例代码完全不需要变动。
  2. 易于单元测试(Testability):由于应用用例层和核心领域层都是不依赖具体技术的纯 TypeScript 代码,我们可以使用 Mock 轻易地编写极其快速的单元测试,甚至在数据库、外部API尚未准备妥当之前,就能对核心业务流程进行自动化覆盖验证。
  3. 数据库无关(Database Independence):应用层只依赖 UserRepository 抽象接口。底层数据库是从 SQL(PostgreSQL、MySQL)迁移到 NoSQL(MongoDB、DynamoDB),对内层用例完全透明。

总结

干净架构在初期落地时需要编写较多的目录结构和接口契约,但这是一种**“先苦后甜”**的投资。当项目逐渐变大、业务错综复杂、依赖的外部库需要升级或重构时,这套高度解耦的系统将展现出无与伦比的安全感与稳定性。在面对高速发展的业务时,保持核心领域逻辑的纯粹与干净,才是延长后端系统生命周期的唯一法宝。