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

NodeJS+TypeScript MVC架构中,依赖注入构造函数设默认对象是否合规?

问题描述

我正在使用NodeJS和TypeScript开发MVC模式的REST API。在模型层我创建了实体(entities)、服务(services)和仓库(repositories)。控制器层与框架解耦,通过适配器接收请求和响应对象。

我的控制器中有一个需通过构造函数注入的服务,但我创建了一个默认服务,以防未传入任何服务的情况:

import { type HttpResponse } from './Contracts/httpResponse';
import { type HttpRequest } from './Contracts/httpRequest';
import { HealthService } from '../Core/Service/Implementation/health.service';
import type { HealthServiceInterface } from '../Core/Service/Contract/health.service';

export class HealthController {
  private readonly healthService: HealthServiceInterface;

  constructor(healthService?: HealthServiceInterface) {
    this.healthService = healthService ?? new HealthService();
  }

  public async getHealth(request: HttpRequest): Promise<HttpResponse> {
    const service = new HealthService();
    const health = await service.getHealth();

    return {
      statusCode: 200,
      body: health
    }
  }
}

在服务层我也做了同样的处理:接收一个仓库作为参数,但如果参数未定义,我会实例化一个标准仓库:

import type { HealthRepositoryInterface } from "../../Repository/Contract/health.repository";
import type { HealthServiceInterface } from "../Contract/health.service";
import type { Health } from "../../Entity/health.entity";
import { HealthRepository } from "../../Repository/Implementation/health.repository";

export class HealthService implements HealthServiceInterface {
  private readonly healthRepository: HealthRepositoryInterface;

  constructor(healthRepository?: HealthRepositoryInterface) {
    this.healthRepository = healthRepository ?? new HealthRepository();
  }

  public async getHealth(): Promise<Health> {
    const health: Health = await this.healthRepository.getHealth();
    return health
  }
}

我的问题是:这种实现方式是否正确?我是否存在某些架构错误?


回答

你的核心分层思路是合理的,但存在几个关键细节问题需要修正:

1. 控制器中注入的服务未被实际使用

在HealthController的getHealth方法里,你重新实例化了new HealthService(),完全忽略了构造函数中注入(或默认创建)的this.healthService。这直接让依赖注入失去意义——比如你想在测试时注入Mock服务,这个方法根本不会用到它。

修正方案:直接使用类成员变量:

public async getHealth(request: HttpRequest): Promise<HttpResponse> {
  const health = await this.healthService.getHealth();

  return {
    statusCode: 200,
    body: health
  }
}

2. 构造函数硬编码默认实现的潜在问题

你在构造函数里用??兜底实例化具体实现(比如new HealthService()、new HealthRepository()),这种做法有明显的利弊:

  • 优点:类可以独立实例化,不需要外部DI容器就能运行,降低了入门门槛。
  • 缺点:
    • 违反了依赖倒置原则的核心精神——高层模块(控制器/服务)依然直接依赖底层具体实现,而非完全依赖抽象。
    • 如果后续需要替换默认实现,必须修改类的构造函数代码,不符合开闭原则。
    • 测试时如果忘记传入Mock实例,会自动使用真实实现,可能导致测试依赖外部资源(比如数据库)。

优化建议

如果你的项目已经在使用DI容器(比如TypeDI、InversifyJS),建议移除构造函数中的默认实例化,强制通过DI容器注入依赖:

// 控制器构造函数改为必填参数
constructor(private readonly healthService: HealthServiceInterface) {}

// 服务构造函数同理
constructor(private readonly healthRepository: HealthRepositoryInterface) {}

如果暂时不想引入DI容器,希望保持类的可独立运行性,可以把默认实现的创建逻辑抽离到类外部,比如通过静态工厂方法:

export class HealthController {
  private readonly healthService: HealthServiceInterface;

  private constructor(healthService: HealthServiceInterface) {
    this.healthService = healthService;
  }

  // 静态工厂方法提供默认实例
  public static create(): HealthController {
    return new HealthController(new HealthService());
  }

  // 用于测试或自定义注入的工厂方法
  public static createWithService(healthService: HealthServiceInterface): HealthController {
    return new HealthController(healthService);
  }

  // ... getHealth方法
}

这种方式既保留了类的易用性,又避免了构造函数中硬编码具体实现的耦合问题。

3. 架构分层的合理性

你的模型层拆分了实体、服务、仓库,控制器层与框架解耦,这部分是符合MVC+DDD的分层思想的,没有架构层面的错误,继续保持即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 07:23:14