Skip to content

模块划分:领域驱动设计(DDD)在 Nest 中的落地

领域驱动设计(Domain-Driven Design,DDD)是一种软件开发方法,强调以业务领域为核心进行系统设计。在 NestJS 中,模块系统为 DDD 的实施提供了天然的支持。通过合理的模块划分,我们可以构建出更加清晰、可维护和可扩展的应用程序。本文将深入探讨如何在 NestJS 中落地 DDD,明确 UserModuleOrderModuleSharedModule 等模块的职责边界。

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 的关键要点:

  1. 模块划分:按照业务领域划分模块,明确职责边界
  2. 分层架构:实现接口层、应用层、领域层、基础设施层的分离
  3. 依赖管理:通过模块导入和导出机制管理跨模块依赖
  4. 领域模型:使用实体、值对象、聚合根等概念建模业务领域
  5. 仓储模式:抽象数据访问,实现领域层与基础设施层的分离
  6. 领域事件:通过事件机制实现模块间松耦合通信

DDD 在 NestJS 中的优势:

  1. 业务聚焦:使代码更贴近业务需求
  2. 可维护性:清晰的结构便于理解和维护
  3. 可扩展性:模块化设计支持系统演进
  4. 团队协作:明确的职责边界便于团队分工
  5. 测试友好:分层架构便于单元测试和集成测试

通过合理应用 DDD 原则,我们可以构建出更加健壮、可维护和可扩展的 NestJS 应用程序。

在下一篇文章中,我们将探讨 CQRS 模式:Command 与 Query 的分离实现,了解如何使用 @nestjs/cqrs 实现事件溯源与命令总线。