模块划分:领域驱动设计(DDD)在 Nest 中的落地
领域驱动设计(Domain-Driven Design,DDD)是一种软件开发方法,强调以业务领域为核心进行系统设计。在 NestJS 中,模块系统为 DDD 的实施提供了天然的支持。通过合理的模块划分,我们可以构建出更加清晰、可维护和可扩展的应用程序。本文将深入探讨如何在 NestJS 中落地 DDD,明确 UserModule、OrderModule、SharedModule 等模块的职责边界。
1. DDD 基础概念
1.1 什么是领域驱动设计?
领域驱动设计是一种软件开发方法论,强调业务领域和业务逻辑的重要性:
typescript
// 传统分层架构
// 表现层 → 业务逻辑层 → 数据访问层 → 数据库
// DDD 分层架构
// 表现层
// ↓
// 应用层
// ↓
// 领域层 ←→ 基础设施层
// ↓
// 数据库1.2 DDD 核心概念
typescript
// DDD 核心组件
// 1. 实体(Entity)- 具有唯一标识的对象
// 2. 值对象(Value Object)- 没有唯一标识,通过属性区分的对象
// 3. 聚合根(Aggregate Root)- 聚合的入口点
// 4. 仓储(Repository)- 数据访问抽象
// 5. 领域服务(Domain Service)- 跨实体的业务逻辑
// 6. 应用服务(Application Service)- 协调领域对象完成业务用例2. NestJS 中的 DDD 模块结构
2.1 典型模块划分
typescript
// DDD 风格的模块结构
src/
├── user/ // 用户领域
│ ├── user.module.ts // 用户模块
│ ├── domain/ // 领域层
│ │ ├── entities/ // 实体
│ │ ├── value-objects/ // 值对象
│ │ ├── repositories/ // 仓储接口
│ │ └── services/ // 领域服务
│ ├── application/ // 应用层
│ │ ├── services/ // 应用服务
│ │ └── dtos/ // 数据传输对象
│ ├── infrastructure/ // 基础设施层
│ │ └── repositories/ // 仓储实现
│ └── interfaces/ // 接口层
│ └── controllers/ // 控制器
├── order/ // 订单领域
│ ├── order.module.ts
│ ├── domain/
│ ├── application/
│ ├── infrastructure/
│ └── interfaces/
├── shared/ // 共享内核
│ ├── shared.module.ts
│ ├── domain/
│ ├── infrastructure/
│ └── interfaces/
└── app.module.ts // 应用根模块2.2 用户模块实现
typescript
// user/domain/entities/user.entity.ts
export class User {
private readonly id: string;
private name: string;
private email: string;
private createdAt: Date;
private updatedAt: Date;
constructor(
id: string,
name: string,
email: string,
) {
this.id = id;
this.name = name;
this.email = email;
this.createdAt = new Date();
this.updatedAt = new Date();
}
// 实体行为
updateName(name: string): void {
this.name = name;
this.updatedAt = new Date();
}
updateEmail(email: string): void {
this.email = email;
this.updatedAt = new Date();
}
// Getter 方法
getId(): string {
return this.id;
}
getName(): string {
return this.name;
}
getEmail(): string {
return this.email;
}
}
// user/domain/value-objects/email.vo.ts
export class Email {
private readonly value: string;
constructor(email: string) {
if (!this.isValid(email)) {
throw new Error('Invalid email format');
}
this.value = email.toLowerCase();
}
private isValid(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
getValue(): string {
return this.value;
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
// user/domain/repositories/user.repository.ts
export interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// user/domain/services/user-domain.service.ts
export class UserDomainService {
constructor(private readonly userRepository: UserRepository) {}
async isEmailUnique(email: string, excludeUserId?: string): Promise<boolean> {
const existingUser = await this.userRepository.findByEmail(email);
if (!existingUser) {
return true;
}
return excludeUserId ? existingUser.getId() === excludeUserId : false;
}
}3. 模块职责边界
3.1 UserModule 职责
typescript
// user/user.module.ts
@Module({
imports: [
// 导入共享模块
SharedModule,
],
controllers: [
// 接口层:HTTP 控制器
UserController,
UserProfileController,
],
providers: [
// 应用层:应用服务
UserService,
UserProfileService,
// 领域层:领域服务
UserDomainService,
// 基础设施层:仓储实现
{
provide: 'UserRepository',
useClass: UserDatabaseRepository,
},
],
exports: [
// 导出应用服务供其他模块使用
UserService,
UserProfileService,
],
})
export class UserModule {}
// user/application/services/user.service.ts
@Injectable()
export class UserService {
constructor(
@Inject('UserRepository')
private readonly userRepository: UserRepository,
private readonly userDomainService: UserDomainService,
) {}
async createUser(createUserDto: CreateUserDto): Promise<User> {
// 验证业务规则
if (!await this.userDomainService.isEmailUnique(createUserDto.email)) {
throw new ConflictException('Email already exists');
}
// 创建实体
const user = new User(
uuidv4(),
createUserDto.name,
createUserDto.email,
);
// 持久化
await this.userRepository.save(user);
return user;
}
async updateUser(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
// 更新实体
if (updateUserDto.name) {
user.updateName(updateUserDto.name);
}
if (updateUserDto.email) {
if (!await this.userDomainService.isEmailUnique(updateUserDto.email, id)) {
throw new ConflictException('Email already exists');
}
user.updateEmail(updateUserDto.email);
}
// 持久化
await this.userRepository.save(user);
return user;
}
async getUserById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}3.2 OrderModule 职责
typescript
// order/order.module.ts
@Module({
imports: [
// 导入用户模块以使用 UserService
UserModule,
SharedModule,
],
controllers: [
OrderController,
OrderAdminController,
],
providers: [
OrderService,
OrderAdminService,
OrderDomainService,
{
provide: 'OrderRepository',
useClass: OrderDatabaseRepository,
},
],
exports: [
OrderService,
],
})
export class OrderModule {}
// order/domain/entities/order.entity.ts
export class Order {
private readonly id: string;
private userId: string;
private items: OrderItem[];
private status: OrderStatus;
private totalAmount: number;
private createdAt: Date;
private updatedAt: Date;
constructor(
id: string,
userId: string,
items: OrderItem[],
) {
this.id = id;
this.userId = userId;
this.items = items;
this.status = OrderStatus.PENDING;
this.totalAmount = this.calculateTotal();
this.createdAt = new Date();
this.updatedAt = new Date();
}
private calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.getTotalPrice(), 0);
}
confirm(): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error('Order cannot be confirmed');
}
this.status = OrderStatus.CONFIRMED;
this.updatedAt = new Date();
}
cancel(): void {
if (this.status === OrderStatus.SHIPPED || this.status === OrderStatus.DELIVERED) {
throw new Error('Order cannot be cancelled');
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = new Date();
}
ship(): void {
if (this.status !== OrderStatus.CONFIRMED) {
throw new Error('Order must be confirmed before shipping');
}
this.status = OrderStatus.SHIPPED;
this.updatedAt = new Date();
}
// Getter 方法
getId(): string {
return this.id;
}
getUserId(): string {
return this.userId;
}
getStatus(): OrderStatus {
return this.status;
}
getTotalAmount(): number {
return this.totalAmount;
}
}
// order/domain/value-objects/order-item.vo.ts
export class OrderItem {
private readonly productId: string;
private readonly quantity: number;
private readonly unitPrice: number;
constructor(productId: string, quantity: number, unitPrice: number) {
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
getTotalPrice(): number {
return this.quantity * this.unitPrice;
}
getProductId(): string {
return this.productId;
}
getQuantity(): number {
return this.quantity;
}
getUnitPrice(): number {
return this.unitPrice;
}
}
enum OrderStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
SHIPPED = 'shipped',
DELIVERED = 'delivered',
CANCELLED = 'cancelled',
}3.3 SharedModule 职责
typescript
// shared/shared.module.ts
@Module({
providers: [
// 共享的基础设施服务
DatabaseService,
LoggerService,
ConfigService,
// 共享的领域服务
NotificationService,
EmailService,
// 共享的工具类
IdGenerator,
DateTimeHelper,
],
exports: [
DatabaseService,
LoggerService,
ConfigService,
NotificationService,
EmailService,
IdGenerator,
DateTimeHelper,
],
})
export class SharedModule {}
// shared/domain/services/notification.service.ts
@Injectable()
export class NotificationService {
constructor(
private readonly emailService: EmailService,
private readonly logger: LoggerService,
) {}
async sendUserRegisteredNotification(user: User): Promise<void> {
try {
await this.emailService.send({
to: user.getEmail(),
subject: 'Welcome to our platform!',
template: 'user-registered',
data: {
userName: user.getName(),
},
});
} catch (error) {
this.logger.error('Failed to send user registered notification', error);
}
}
async sendOrderConfirmationNotification(order: Order, user: User): Promise<void> {
try {
await this.emailService.send({
to: user.getEmail(),
subject: `Order ${order.getId()} Confirmed`,
template: 'order-confirmed',
data: {
userName: user.getName(),
orderId: order.getId(),
totalAmount: order.getTotalAmount(),
},
});
} catch (error) {
this.logger.error('Failed to send order confirmation notification', error);
}
}
}4. 跨模块依赖管理
4.1 依赖注入和模块导入
typescript
// 正确的模块依赖关系
// order/application/services/order.service.ts
@Injectable()
export class OrderService {
constructor(
@Inject('OrderRepository')
private readonly orderRepository: OrderRepository,
// 通过 UserModule 导出的 UserService
private readonly userService: UserService,
private readonly notificationService: NotificationService,
) {}
async createOrder(userId: string, createOrderDto: CreateOrderDto): Promise<Order> {
// 验证用户存在
const user = await this.userService.getUserById(userId);
// 创建订单实体
const items = createOrderDto.items.map(item =>
new OrderItem(item.productId, item.quantity, item.unitPrice)
);
const order = new Order(uuidv4(), userId, items);
// 保存订单
await this.orderRepository.save(order);
// 发送确认通知
await this.notificationService.sendOrderConfirmationNotification(order, user);
return order;
}
}
// AppModule 根模块
@Module({
imports: [
// 按依赖顺序导入模块
SharedModule,
UserModule,
OrderModule,
// 其他模块...
],
})
export class AppModule {}4.2 循环依赖避免
typescript
// 避免循环依赖的策略
// ❌ 错误示例:UserModule 和 OrderModule 循环依赖
// user.module.ts
@Module({
imports: [OrderModule], // 导入 OrderModule
})
// order.module.ts
@Module({
imports: [UserModule], // 导入 UserModule,形成循环依赖
})
// ✅ 正确示例:通过 SharedModule 共享依赖
// shared.module.ts
@Module({
providers: [
SharedUserService,
SharedOrderService,
],
exports: [
SharedUserService,
SharedOrderService,
],
})
// user.module.ts
@Module({
imports: [SharedModule],
})
// order.module.ts
@Module({
imports: [SharedModule],
})5. 领域事件和集成
5.1 领域事件实现
typescript
// shared/domain/events/domain-event.interface.ts
export interface DomainEvent {
readonly eventName: string;
readonly occurredAt: Date;
}
// user/domain/events/user-registered.event.ts
export class UserRegisteredEvent implements DomainEvent {
readonly eventName = 'UserRegistered';
readonly occurredAt: Date;
constructor(
readonly userId: string,
readonly userName: string,
readonly userEmail: string,
) {
this.occurredAt = new Date();
}
}
// order/domain/events/order-confirmed.event.ts
export class OrderConfirmedEvent implements DomainEvent {
readonly eventName = 'OrderConfirmed';
readonly occurredAt: Date;
constructor(
readonly orderId: string,
readonly userId: string,
readonly totalAmount: number,
) {
this.occurredAt = new Date();
}
}
// shared/infrastructure/event-bus/event-bus.service.ts
@Injectable()
export class EventBus {
private readonly handlers = new Map<string, Function[]>();
subscribe(eventName: string, handler: Function): void {
if (!this.handlers.has(eventName)) {
this.handlers.set(eventName, []);
}
this.handlers.get(eventName).push(handler);
}
async publish(event: DomainEvent): Promise<void> {
const handlers = this.handlers.get(event.eventName) || [];
for (const handler of handlers) {
await handler(event);
}
}
}5.2 事件处理
typescript
// user/application/handlers/user-registered.handler.ts
@Injectable()
export class UserRegisteredHandler {
constructor(
private readonly notificationService: NotificationService,
) {}
@EventHandler(UserRegisteredEvent)
async handle(event: UserRegisteredEvent): Promise<void> {
// 发送欢迎邮件
await this.notificationService.sendUserRegisteredNotification(
new User(event.userId, event.userName, event.userEmail)
);
}
}
// order/application/handlers/order-confirmed.handler.ts
@Injectable()
export class OrderConfirmedHandler {
constructor(
private readonly notificationService: NotificationService,
) {}
@EventHandler(OrderConfirmedEvent)
async handle(event: OrderConfirmedEvent): Promise<void> {
// 发送订单确认通知
const user = await this.userService.getUserById(event.userId);
const order = await this.orderRepository.findById(event.orderId);
await this.notificationService.sendOrderConfirmationNotification(order, user);
}
}6. 最佳实践和模式
6.1 模块组织原则
typescript
// 模块组织最佳实践
// 1. 单一职责原则:每个模块负责一个明确的业务领域
// 2. 高内聚低耦合:模块内部高度相关,模块间依赖清晰
// 3. 依赖倒置:依赖抽象而非具体实现
// 4. 开闭原则:对扩展开放,对修改关闭
// 模块边界清晰示例
// ✅ 好的设计
@Module({
imports: [SharedModule],
controllers: [UserController],
providers: [
UserService,
{
provide: 'UserRepository',
useClass: UserDatabaseRepository,
},
],
exports: [UserService],
})
export class UserModule {}
// ❌ 不好的设计
@Module({
imports: [OrderModule, PaymentModule, NotificationModule], // 过多依赖
controllers: [UserController, OrderController, PaymentController], // 职责不清
providers: [UserService, OrderService, PaymentService, NotificationService], // 混合职责
})
export class MixedModule {}6.2 分层架构实现
typescript
// 分层架构实现
// 1. 接口层(Interfaces):处理外部交互
// 2. 应用层(Application):协调领域对象完成业务用例
// 3. 领域层(Domain):核心业务逻辑
// 4. 基础设施层(Infrastructure):技术实现细节
// 接口层:控制器
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async createUser(@Body() createUserDto: CreateUserDto) {
// 调用应用服务,不包含业务逻辑
return this.userService.createUser(createUserDto);
}
}
// 应用层:应用服务
@Injectable()
export class UserService {
constructor(
@Inject('UserRepository') private readonly userRepository: UserRepository,
private readonly userDomainService: UserDomainService,
) {}
async createUser(createUserDto: CreateUserDto): Promise<User> {
// 协调领域对象完成业务用例
if (!await this.userDomainService.isEmailUnique(createUserDto.email)) {
throw new ConflictException('Email already exists');
}
const user = new User(uuidv4(), createUserDto.name, createUserDto.email);
await this.userRepository.save(user);
return user;
}
}
// 领域层:实体和领域服务
export class User {
// 实体包含业务行为
updateEmail(email: string): void {
// 验证业务规则
if (!Email.isValid(email)) {
throw new Error('Invalid email');
}
this.email = email;
}
}
// 基础设施层:仓储实现
@Injectable()
export class UserDatabaseRepository implements UserRepository {
constructor(private readonly prisma: PrismaService) {}
async save(user: User): Promise<void> {
// 数据库操作细节
await this.prisma.user.upsert({
where: { id: user.getId() },
create: {
id: user.getId(),
name: user.getName(),
email: user.getEmail(),
},
update: {
name: user.getName(),
email: user.getEmail(),
},
});
}
}7. 总结
在 NestJS 中实施 DDD 的关键要点:
- 模块划分:按照业务领域划分模块,明确职责边界
- 分层架构:实现接口层、应用层、领域层、基础设施层的分离
- 依赖管理:通过模块导入和导出机制管理跨模块依赖
- 领域模型:使用实体、值对象、聚合根等概念建模业务领域
- 仓储模式:抽象数据访问,实现领域层与基础设施层的分离
- 领域事件:通过事件机制实现模块间松耦合通信
DDD 在 NestJS 中的优势:
- 业务聚焦:使代码更贴近业务需求
- 可维护性:清晰的结构便于理解和维护
- 可扩展性:模块化设计支持系统演进
- 团队协作:明确的职责边界便于团队分工
- 测试友好:分层架构便于单元测试和集成测试
通过合理应用 DDD 原则,我们可以构建出更加健壮、可维护和可扩展的 NestJS 应用程序。
在下一篇文章中,我们将探讨 CQRS 模式:Command 与 Query 的分离实现,了解如何使用 @nestjs/cqrs 实现事件溯源与命令总线。