4. 模块化设计原则:Feature Module 与 Shared Module 划分

在 NestJS 中,模块是组织代码的核心单元。合理的模块划分不仅影响代码的可维护性,更直接决定了事务边界的清晰度与 API 设计的稳定性。本章将深入探讨 Feature Module 与 Shared Module 的划分原则、模块的导入导出机制,以及全局模块与动态模块的高级用法。

4.1 Feature Module 与 Shared Module 的职责边界

NestJS 的模块体系可以划分为两大类:Feature Module(特性模块) 和 Shared Module(共享模块)。两者的职责对比如下:

维度 Feature Module Shared Module
核心职责 封装特定业务领域(如订单、用户、支付) 提供跨模块复用的基础设施(如数据库连接、日志、工具类)
事务边界 通常拥有独立的数据源或聚合根,事务范围明确 不持有业务事务,仅提供无状态服务或单例资源
导出策略 按需导出 Service、Controller、Entity 导出 Provider、Guard、Interceptor、Pipe 等通用组件
生命周期 随业务迭代频繁变化 相对稳定,变更频率低
典型示例 OrderModule、UserModule、PaymentModule DatabaseModule、LoggerModule、ConfigModule
设计原则:Feature Module 应保持高内聚、低耦合,每个模块只关注一个业务聚合根;Shared Module 应无业务语义,只提供技术基础设施。

4.2 模块的导入导出机制

NestJS 的模块系统通过 imports、exports、providers、controllers 四个核心属性来组织依赖关系。理解导入导出机制是构建可维护模块体系的基础。

4.2.1 基本导入导出规则

  • 模块 A 导入模块 B:A 可以访问 B 中 exports 导出的 Provider。
  • 模块 B 导出 Provider:只有 B 的 exports 数组中列出的 Provider 才能被其他模块使用。
  • 循环依赖:NestJS 支持通过 forwardRef 解决循环依赖,但应尽量避免。
// user.module.ts - Feature Module 示例
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserEntity } from './entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],  // 导入 TypeORM 特性
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],  // 仅导出 UserService,供其他模块使用
})
export class UserModule {}
// database.module.ts - Shared Module 示例
import { Module, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseConfigService } from './database-config.service';

@Global()  // 全局模块,无需在每个模块中重复导入
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useClass: DatabaseConfigService,
    }),
  ],
  providers: [DatabaseConfigService],
  exports: [DatabaseConfigService],
})
export class DatabaseModule {}

4.2.2 导入导出机制的事务边界影响

在 API Design 驱动的事务治理中,模块的导入导出直接决定了事务的传播范围:

  • 跨模块事务:如果模块 A 导入了模块 B 的 Service,并在同一个方法中调用 B 的数据库操作,事务边界会跨越两个模块。此时需要明确事务的传播行为(REQUIRED、REQUIRES_NEW 等)。
  • 模块内事务:推荐将事务边界控制在单个 Feature Module 内部,跨模块调用时通过 API 网关或事件驱动解耦。
最佳实践:在 Feature Module 中,使用 @Transactional() 装饰器明确标注事务边界;Shared Module 中的 Service 不应直接参与业务事务,而是通过回调或事件机制与业务层交互。

4.3 全局模块与动态模块

全局模块和动态模块是 NestJS 模块系统的高级特性,在大型项目中合理使用可以显著提升架构的灵活性和可维护性。

4.3.1 全局模块(Global Module)

使用 @Global() 装饰器标记的模块会自动注册到全局作用域,其导出的 Provider 可以被所有模块直接使用,无需在 imports 中重复导入。

特性 说明 适用场景
自动注册 全局模块只需在根模块导入一次 数据库连接、日志、配置中心
单例保证 全局模块的 Provider 在整个应用中是单例 缓存服务、事件总线
事务风险 全局模块的 Service 可能被多个模块注入,事务边界容易模糊 谨慎用于业务逻辑,优先用于基础设施
注意:全局模块虽然方便,但过度使用会导致隐式依赖,降低模块的可测试性和可读性。建议仅将真正全局唯一的资源(如数据库连接池、配置服务)声明为全局模块。

4.3.2 动态模块(Dynamic Module)

动态模块允许在运行时根据配置参数动态注册 Provider,是实现可配置化、插件化架构的关键技术。

// config.module.ts - 动态模块示例
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

export interface ConfigModuleOptions {
  path: string;
  isGlobal?: boolean;
}

@Module({})
export class ConfigModule {
  static register(options: ConfigModuleOptions): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
      global: options.isGlobal ?? false,  // 支持动态设置全局性
    };
  }
}

// 使用方式
// @Module({
//   imports: [ConfigModule.register({ path: '.env', isGlobal: true })],
// })

4.4 模块划分与事务边界的映射关系

为了直观展示模块划分如何影响事务边界,下图描述了典型 NestJS 应用的模块拓扑与事务传播路径:

模块划分与事务边界映射关系 AppModule (根模块) OrderModule 事务边界 A UserModule 事务边界 B PaymentModule 事务边界 C DatabaseModule 全局模块 (无事务) 跨模块事务调用 事务边界治理要点: 1. 每个 Feature Module 拥有独立的事务边界,避免跨模块直接调用 DAO 层。 2. Shared Module 不参与业务事务,仅提供无状态基础设施。 3. 跨模块业务协作应通过 API 调用或消息队列解耦,而非共享事务。 4. 全局模块的 Provider 注入需谨慎,避免隐式事务传播。

4.5 模块设计检查清单

在实际项目中,可以使用以下清单来评估模块设计的合理性:

检查项 通过标准 常见反例
单一职责 每个模块只封装一个业务聚合根 一个模块同时处理订单和用户
事务边界清晰 模块内事务范围明确,不跨模块传播 Service 中同时调用两个模块的 Repository
导出最小化 只导出必要的 Provider,隐藏内部实现 导出所有 Provider,包括内部工具类
全局模块节制 全局模块数量不超过 3 个 将业务模块也声明为全局模块
动态模块可配置 动态模块支持运行时参数注入 动态模块硬编码配置,失去动态意义

4.6 动态模块在事务治理中的高级应用

动态模块不仅可以用于配置化,还可以在事务治理中实现按需注册事务管理器:

// transaction.module.ts - 动态事务模块
import { DynamicModule, Module } from '@nestjs/common';
import { TransactionService } from './transaction.service';

export interface TransactionModuleOptions {
  isolationLevel?: 'READ_COMMITTED' | 'REPEATABLE_READ' | 'SERIALIZABLE';
  timeout?: number;
}

@Module({})
export class TransactionModule {
  static forRoot(options: TransactionModuleOptions = {}): DynamicModule {
    return {
      module: TransactionModule,
      providers: [
        {
          provide: 'TRANSACTION_OPTIONS',
          useValue: options,
        },
        TransactionService,
      ],
      exports: [TransactionService],
      global: true,
    };
  }

  static forFeature(options: TransactionModuleOptions = {}): DynamicModule {
    return {
      module: TransactionModule,
      providers: [
        {
          provide: 'TRANSACTION_OPTIONS',
          useValue: options,
        },
        TransactionService,
      ],
      exports: [TransactionService],
    };
  }
}

// 在根模块中设置全局默认事务隔离级别
// @Module({
//   imports: [TransactionModule.forRoot({ isolationLevel: 'READ_COMMITTED' })],
// })

// 在特定 Feature Module 中覆盖事务配置
// @Module({
//   imports: [TransactionModule.forFeature({ isolationLevel: 'SERIALIZABLE' })],
// })
设计思路:通过 forRoot 和 forFeature 的区分,可以在全局层面设定默认事务策略,同时在特定业务模块中按需调整隔离级别,实现精细化的 API 事务治理。

4.7 总结

模块化设计是 NestJS 架构的基石,也是 API Design 驱动事务边界治理的第一道防线。通过合理划分 Feature Module 与 Shared Module、掌握导入导出机制、审慎使用全局模块与动态模块,可以构建出事务边界清晰、易于维护和扩展的应用架构。下一层我们将深入探讨 Provider 的作用域与依赖注入对事务传播的影响。