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,校验是否存在关联的学习记录,若存在则抛出异常阻止删除。
- FinancesModule订阅
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
相关产品推荐
相关产品推荐

