在 Nest.js 开发中,依赖注入(DI)是一个核心概念。开发者经常会遇到两种注入方式的选择:构造函数注入和属性注入。本文将比较这两种方式的区别,帮助你在实际开发中做出更好的选择。
@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();
}
}
@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();
}
}
readonly
修饰符可以确保这些依赖项在实例化后不会被修改。让我们通过之前的代码示例来具体分析两种注入方式的区别:
构造函数注入:
@Injectable()
export class CatsService {
constructor(
@InjectRepository(Cat)
private readonly catRepository: Repository<Cat>,
private readonly configService: ConfigService,
private readonly logger: LoggerService
) {}
}
readonly
确保依赖不会被修改属性注入:
@Injectable()
export class CatsService {
@InjectRepository(Cat)
private catRepository: Repository<Cat>;
@Inject()
private configService: ConfigService;
private logger = new Logger(CatsService.name);
}
readonly
以下场景适合使用构造函数注入:
@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 });
}
}
以下场景适合使用属性注入:
@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');
}
// ... 核心业务逻辑
}
}
构造函数注入的测试:
describe('CatsService', () => {
let service: CatsService;
beforeEach(() => {
// 所有依赖必须在创建实例时提供,更容易发现遗漏
service = new CatsService(
mockCatRepository,
mockConfigService,
mockLogger
);
});
});
属性注入的测试:
describe('CatsService', () => {
let service: CatsService;
beforeEach(() => {
service = new CatsService();
// 需要手动注入每个依赖,容易遗漏
service.catRepository = mockCatRepository;
service.configService = mockConfigService;
// 如果忘记注入某个依赖,可能到运行时才发现
});
});
在实际开发中,构造函数注入通常是首选,因为它更简洁、强类型和易于维护。属性注入则适用于需要更高灵活性的场景。选择哪种方式取决于具体需求和代码风格。
优先使用构造函数注入
合理使用属性注入
混合使用的场景
Q: 为什么不能全部使用属性注入? A: 属性注入可能导致依赖关系不明确,且不利于单元测试。
Q: 构造函数参数过多怎么办? A: 这通常是类职责过重的信号,考虑拆分服务或使用外观模式。