NestJS六边形架构下依赖倒置的实现问题
NestJS依赖倒置+六边形架构的依赖注入问题解决方案
问题背景
在NestJS中尝试落地依赖倒置原则与六边形架构,设计了四层模块结构,但当前实现出现依赖注入报错,若直接在应用模块导入数据库模块会破坏依赖倒置原则,需寻求合规解决方案。
预期架构目标
- 根模块:
AppModule,负责整合所有模块 - 数据库模块:
InfraDatabaseModule,不导入其他模块,仅由根模块直接导入 - REST模块:
InfraRestModule,导入应用模块以让控制器调用其中服务 - 应用模块:
ApplicationModule,作为最内层模块不导入其他模块,通过Repository接口操作实体,无需知晓接口具体实现(依赖倒置)
现有实现代码
app.module.ts
@Module({ imports: [ InfraRestModule, // API请求入口 InfraDatabaseModule, // 实现Repository接口 ], }) export class AppModule {}
infra-rest.module.ts
@Module({ imports: [ApplicationModule], controllers: [SomeController], // 控制器注入应用模块的服务 }) export class InfraRestModule {}
infra-database.module.ts
@Module({ providers: [{ provide: MyRepoSymbol, useClass: MyRepoImpl }], exports: [MyRepoSymbol], }) export class InfraDatabaseModule {}
application.module.ts
@Module({ providers: [SomeService], exports: [SomeService], }) export class ApplicationModule {} // 模块内其他文件 @Injectable() export class SomeService { constructor(@Inject(MyRepoSymbol) private readonly repo: MyRepo) {} // ... }
interfaces/my-repo.ts
export interface MyRepo { // ... } export const MyRepoSymbol = Symbol('MyRepoSymbol');
报错信息
UnknownDependenciesException [Error]: Nest can't resolve dependencies of the SomeService (?). Please make sure that the argument Symbol(MyRepoSymbol) at index [0] is available in the ApplicationModule context. Potential solutions: - Is ApplicationModule a valid NestJS module? - If Symbol(MyRepoSymbol) is a provider, is it part of the current ApplicationModule? - If Symbol(MyRepoSymbol) is exported from a separate @Module, is that module imported within ApplicationModule? @Module({ imports: [ /* the Module containing Symbol(HELLO_TEST) */ ] })
解决方案
核心思路是利用NestJS的模块作用域与导出机制,让根模块作为基础设施层与应用层的桥梁,既满足依赖注入的上下文要求,又保持应用层的独立性。
方案1:根模块中转基础设施提供者(推荐)
修改AppModule导出数据库模块,让InfraRestModule通过导入根模块获取基础设施提供者:
修改后的app.module.ts
@Module({ imports: [InfraDatabaseModule], exports: [InfraDatabaseModule], // 导出数据库模块,使导入根模块的模块能访问其提供者 }) export class AppModule {}
修改后的infra-rest.module.ts
@Module({ imports: [AppModule, ApplicationModule], // 同时导入根模块(获取数据库实现)和应用模块 controllers: [SomeController], }) export class InfraRestModule {}
此时ApplicationModule无需导入任何基础设施模块,SomeService能在InfraRestModule的上下文里获取到MyRepoSymbol的实现,完全符合依赖倒置原则。
方案2:标记数据库模块为全局模块
在InfraDatabaseModule上添加@Global()装饰器,使其导出的提供者在全应用范围内可用:
修改后的infra-database.module.ts
@Global() // 标记为全局模块 @Module({ providers: [{ provide: MyRepoSymbol, useClass: MyRepoImpl }], exports: [MyRepoSymbol], }) export class InfraDatabaseModule {}
这种方式无需修改其他模块,但全局模块需谨慎使用,避免过度耦合。
方案3:创建基础设施聚合模块(适用于大型项目)
新建InfraModule统一管理所有基础设施模块,再由根模块导出:
infra.module.ts
@Module({ imports: [InfraDatabaseModule], exports: [InfraDatabaseModule], }) export class InfraModule {}
修改后的app.module.ts
@Module({ imports: [InfraModule], exports: [InfraModule], }) export class AppModule {}
修改后的infra-rest.module.ts
@Module({ imports: [AppModule, ApplicationModule], controllers: [SomeController], }) export class InfraRestModule {}
该方案能更清晰地划分基础设施层边界,适合架构复杂的项目。
关键原理
NestJS的模块作用域是层级化的:当模块A导入模块B时,模块A及其子模块可访问模块B导出的提供者。通过根模块作为中间层,既能让应用层获取到基础设施实现,又能保证应用层不依赖具体实现,完美契合依赖倒置原则与六边形架构的核心要求。
内容的提问来源于stack exchange,提问作者Bram Meerten
相关产品推荐
相关产品推荐

