logo
火山博客
导航

Nest中属性注入和依赖注入的区别

2025-01-14
9阅读时间5分钟

前言

在 Nest.js 开发中,依赖注入(DI)是一个核心概念。开发者经常会遇到两种注入方式的选择:构造函数注入和属性注入。本文将比较这两种方式的区别,帮助你在实际开发中做出更好的选择。

两种注入方式的示例对比

构造函数注入示例

Typescript
@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private readonly catRepository: Repository<Cat>,
    private readonly configService: ConfigService,
    private readonly logger: LoggerService
  ) {}

  async findAll() {
    this.logger.log('Finding all cats');
    return this.catRepository.find();
  }
}

属性注入示例

Typescript
@Injectable()
export class CatsService {
  @InjectRepository(Cat)
  private catRepository: Repository<Cat>;

  @Inject()
  private configService: ConfigService;

  private logger = new Logger(CatsService.name);

  async findAll() {
    this.logger.log('Finding all cats');
    return this.catRepository.find();
  }
}

使用构造函数注入

特点和优势:

  1. 简洁性​:所有依赖项都在构造函数参数中注入,使得依赖项一目了然。
  2. 不可变性​:使用 readonly 修饰符可以确保这些依赖项在实例化后不会被修改。
  3. 强类型检查​:TypeScript 能够更好地进行类型检查,确保所有依赖项在类实例化时都正确提供。
  4. 依赖注入控制​:构造函数注入依赖项时,可以在类实例化时立即提供这些依赖项。
  5. 测试友好性​:构造函数注入使得单元测试更容易,因为可以明确地模拟(mock)所有依赖项。

使用属性注入

特点和优势:

  1. 灵活性​:可以在类的任何地方注入依赖项,不局限于构造函数中。
  2. 延迟初始化​:某些依赖项可能需要延迟初始化,属性注入可以更灵活地处理这些情况。

比较和选择

让我们通过之前的代码示例来具体分析两种注入方式的区别:

1. 代码结构对比

构造函数注入:

Typescript
@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private readonly catRepository: Repository<Cat>,
    private readonly configService: ConfigService,
    private readonly logger: LoggerService
  ) {}
}
  • 所有依赖在一处声明,依赖关系一目了然
  • readonly 确保依赖不会被修改
  • 如果忘记注入某个依赖,TypeScript 会在编译时报错

属性注入:

Typescript
@Injectable()
export class CatsService {
  @InjectRepository(Cat)
  private catRepository: Repository<Cat>;

  @Inject()
  private configService: ConfigService;

  private logger = new Logger(CatsService.name);
}
  • 依赖分散在类的不同位置
  • 可以灵活选择是否使用 readonly
  • 即使忘记注入某个依赖,也可能到运行时才发现问题

2. 实际应用场景

以下场景适合使用构造函数注入:

Typescript
@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private readonly catRepository: Repository<Cat>,
    private readonly configService: ConfigService,
  ) {}

  async findAll() {
    // catRepository 和 configService 是核心依赖
    // 没有它们服务就无法正常工作
    const limit = this.configService.get('CATS_QUERY_LIMIT');
    return this.catRepository.find({ take: limit });
  }
}

以下场景适合使用属性注入:

Typescript
@Injectable()
export class CatsService {
  // 可选的日志记录器,即使没有也不影响核心功能
  @Inject()
  private readonly logger?: LoggerService;

  // 工具类,可以延迟初始化
  private readonly helper = new CatsHelper();

  async findAll() {
    if (this.logger) {
      this.logger.log('Finding all cats');
    }
    // ... 核心业务逻辑
  }
}

3. 测试场景对比

构造函数注入的测试:

Typescript
describe('CatsService', () => {
  let service: CatsService;

  beforeEach(() => {
    // 所有依赖必须在创建实例时提供,更容易发现遗漏
    service = new CatsService(
      mockCatRepository,
      mockConfigService,
      mockLogger
    );
  });
});

属性注入的测试:

Typescript
describe('CatsService', () => {
  let service: CatsService;

  beforeEach(() => {
    service = new CatsService();
    // 需要手动注入每个依赖,容易遗漏
    service.catRepository = mockCatRepository;
    service.configService = mockConfigService;
    // 如果忘记注入某个依赖,可能到运行时才发现
  });
});

结论

在实际开发中,构造函数注入通常是首选,因为它更简洁、强类型和易于维护。属性注入则适用于需要更高灵活性的场景。选择哪种方式取决于具体需求和代码风格。

最佳实践建议

  1. 优先使用构造函数注入

    • 对于必需的依赖,始终使用构造函数注入
    • 保持依赖关系清晰可见
  2. 合理使用属性注入

    • 用于可选依赖
    • 用于需要延迟初始化的场景
    • 避免过度使用,以免降低代码可维护性
  3. 混合使用的场景

    • 核心服务使用构造函数注入
    • 工具类或辅助功能使用属性注入

常见问题

  1. Q: 为什么不能全部使用属性注入? A: 属性注入可能导致依赖关系不明确,且不利于单元测试。

  2. Q: 构造函数参数过多怎么办? A: 这通常是类职责过重的信号,考虑拆分服务或使用外观模式。

参考资料

AI播客
文章概要
AI播客
2024 © Powered by
hsBlog
|
后台管理