You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS项目架构优化:如何避免模块间不合理依赖

基于NestJS的模块解耦通信架构方案

1. 采用事件驱动架构(Event-Driven Architecture)

这是解决跨模块无依赖通信的核心方案,通过发布/订阅事件触发副作用操作,完全避免模块间的直接依赖:

  • 定义全局事件总线:用NestJS内置的EventEmitterModule,或@nestjs/event-emitter第三方库实现全局事件机制。
  • 定义领域事件:比如StudentCreatedEvent、StudentDeletedEvent、StudentJoinedGroupEvent、SubjectDeleteAttemptEvent等,每个事件携带必要的上下文数据(如学生ID、组ID、科目ID)。
  • 模块内发布事件:在StudentService.create()完成学生创建后,发布StudentCreatedEvent;在StudentService.joinGroup()发布StudentJoinedGroupEvent;在SubjectService.delete()前发布SubjectDeleteAttemptEvent。
  • 其他模块订阅事件:
    • FinancesModule订阅StudentCreatedEvent,触发财务记录生成逻辑;
    • ScoresModule订阅StudentJoinedGroupEvent,创建空白成绩单;
    • GroupModule订阅StudentDeletedEvent,移除该学生的所有组关联;
    • 所有涉及科目关联的模块(如ScoresModule)订阅SubjectDeleteAttemptEvent,校验是否存在关联的学习记录,若存在则抛出异常阻止删除。

2. 抽象共享核心模块(Shared Core Module)

把跨模块共用的模型、常量、事件定义、基础工具抽象到独立的CoreModule或SharedModule中:

  • 所有模块依赖CoreModule而非彼此,比如StudentModule和ScoresModule都导入CoreModule来使用事件定义和基础类型。
  • 注意:CoreModule只存放无业务逻辑的纯定义,避免变成大杂烩,保持模块职责单一。

3. 使用CQRS模式(命令查询职责分离)

对于复杂的跨模块业务流程,CQRS能进一步解耦命令执行和副作用处理:

  • 定义命令:比如CreateStudentCommand、DeleteSubjectCommand,携带操作所需的参数。
  • 命令处理器:每个命令对应独立的处理器,处理器内部完成核心业务(如创建学生),然后发布事件触发副作用。
  • 查询与命令分离:读操作直接由各模块的service处理,写操作通过命令触发,避免业务逻辑分散。

4. MongoDB场景下的校验逻辑实现

因为无法使用SQL外键,需要在事件订阅中手动校验关联数据:

  • 比如处理SubjectDeleteAttemptEvent时,ScoresService查询是否存在关联该科目的学生成绩记录,若存在则抛出ConflictException,终止删除流程;
  • 可以给MongoDB集合添加索引(如subjectId的单字段索引)来提升校验查询的性能。

具体代码示例片段

1. 配置全局事件总线

在根模块导入事件总线模块:

import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    StudentModule,
    FinancesModule,
    ScoresModule,
    // 其他模块
  ],
})
export class AppModule {}

2. 定义领域事件

在CoreModule中创建事件定义文件:

// src/core/events/student.events.ts
export class StudentCreatedEvent {
  constructor(public readonly studentId: string, public readonly studentInfo: Record<string, any>) {}
}

export class StudentJoinedGroupEvent {
  constructor(public readonly studentId: string, public readonly groupId: string) {}
}

// src/core/events/subject.events.ts
export class SubjectDeleteAttemptEvent {
  constructor(public readonly subjectId: string) {}
}

3. 在StudentService中发布事件

import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { StudentCreatedEvent, StudentJoinedGroupEvent } from '../core/events/student.events';

@Injectable()
export class StudentService {
  constructor(
    private readonly studentRepository: StudentRepository,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async create(studentData: Record<string, any>) {
    const student = await this.studentRepository.create(studentData);
    // 发布学生创建事件
    this.eventEmitter.emit('student.created', new StudentCreatedEvent(student._id.toString(), student));
    return student;
  }

  async joinGroup(studentId: string, groupId: string) {
    // 执行学生加入组的核心逻辑
    await this.studentRepository.updateOne({ _id: studentId }, { $addToSet: { groups: groupId } });
    // 发布学生加入组事件
    this.eventEmitter.emit('student.joined-group', new StudentJoinedGroupEvent(studentId, groupId));
  }
}

4. 在FinancesService中订阅事件

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { StudentCreatedEvent } from '../core/events/student.events';

@Injectable()
export class FinancesService {
  constructor(private readonly financeRepository: FinanceRepository) {}

  @OnEvent('student.created')
  async handleStudentCreated(event: StudentCreatedEvent) {
    // 生成初始财务记录
    await this.financeRepository.create({
      studentId: event.studentId,
      type: 'enrollment',
      amount: 0,
      createdAt: new Date(),
    });
  }
}

5. Subject删除前的校验逻辑

import { Injectable, ConflictException } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { SubjectDeleteAttemptEvent } from '../core/events/subject.events';

@Injectable()
export class SubjectService {
  constructor(
    private readonly subjectRepository: SubjectRepository,
    private readonly eventEmitter: EventEmitter2
  ) {}

  async delete(subjectId: string) {
    // 发布删除尝试事件,触发跨模块校验
    await this.eventEmitter.emitAsync('subject.delete.attempt', new SubjectDeleteAttemptEvent(subjectId));
    
    // 校验通过后执行删除
    await this.subjectRepository.deleteOne({ _id: subjectId });
    return { message: '科目删除成功' };
  }
}

// ScoresService中的订阅校验
@Injectable()
export class ScoresService {
  constructor(private readonly scoresRepository: ScoresRepository) {}

  @OnEvent('subject.delete.attempt', { async: true })
  async checkSubjectInUse(event: SubjectDeleteAttemptEvent) {
    const hasAssociatedScores = await this.scoresRepository.exists({ subjectId: event.subjectId });
    if (hasAssociatedScores) {
      throw new ConflictException('无法删除科目:存在关联的学生成绩记录');
    }
  }
}

内容的提问来源于stack exchange,提问作者darbaidze2020

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 18:45:32