← Назад к вопросам
Как работает принцип Dependency Injection?
2.0 Middle🔥 211 комментариев
#Архитектура и паттерны#ООП
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает принцип Dependency Injection (DI)
Dependency Injection (внедрение зависимостей) — это основной паттерн проектирования, который обеспечивает слабую связанность и высокую тестируемость кода.
Основная идея
Вместо того чтобы компонент сам создавал свои зависимости, они передаются извне. Это позволяет менять реализацию без изменения компонента.
Проблема без DI
// ПЛОХО: сильная связанность
class UserService {
private database = new PostgresDatabase(); // создаем зависимость внутри
private logger = new ConsoleLogger(); // жестко привязаны
createUser(name: string) {
this.logger.log('Creating user');
this.database.insert({ name });
}
}
// Проблемы:
// 1. Сложно тестировать (нельзя использовать mock)
// 2. Сложно переключаться между реализациями
// 3. UserService зависит от конкретных классов
// 4. Если изменится PostgresDatabase, надо менять UserService
Решение с DI
1. Constructor Injection (самый популярный)
// Интерфейсы зависимостей
interface IDatabase {
insert(data: any): void;
}
interface ILogger {
log(message: string): void;
}
// Реализация
class PostgresDatabase implements IDatabase {
insert(data: any) {
console.log('Inserting to Postgres:', data);
}
}
class ConsoleLogger implements ILogger {
log(message: string) {
console.log(message);
}
}
// UserService зависит от интерфейсов, не от конкретных классов
class UserService {
constructor(
private database: IDatabase,
private logger: ILogger
) {}
createUser(name: string) {
this.logger.log('Creating user');
this.database.insert({ name });
}
}
// Использование в production
const database = new PostgresDatabase();
const logger = new ConsoleLogger();
const userService = new UserService(database, logger);
userService.createUser('John');
// Использование в тестах (с mock)
class MockDatabase implements IDatabase {
insert(data: any) { /* ничего не делаем */ }
}
class MockLogger implements ILogger {
log(message: string) { /* не логируем */ }
}
const mockDatabase = new MockDatabase();
const mockLogger = new MockLogger();
const testUserService = new UserService(mockDatabase, mockLogger);
2. Property Injection
class UserService {
database: IDatabase;
logger: ILogger;
setDatabase(db: IDatabase) {
this.database = db;
}
setLogger(log: ILogger) {
this.logger = log;
}
createUser(name: string) {
this.logger.log('Creating user');
this.database.insert({ name });
}
}
const service = new UserService();
service.setDatabase(new PostgresDatabase());
service.setLogger(new ConsoleLogger());
3. Method Injection
class UserService {
createUser(name: string, database: IDatabase, logger: ILogger) {
logger.log('Creating user');
database.insert({ name });
}
}
const service = new UserService();
service.createUser('John', new PostgresDatabase(), new ConsoleLogger());
DI контейнеры
Для автоматического управления зависимостями используют DI контейнеры:
Пример с InversifyJS:
import { Container, inject, injectable } from 'inversify';
// Метаданные зависимостей
const TYPES = {
Database: Symbol.for('Database'),
Logger: Symbol.for('Logger'),
UserService: Symbol.for('UserService')
};
// Декораторы @injectable
@injectable()
class PostgresDatabase implements IDatabase {
insert(data: any) { /* реализация */ }
}
@injectable()
class ConsoleLogger implements ILogger {
log(message: string) { /* реализация */ }
}
@injectable()
class UserService {
constructor(
@inject(TYPES.Database) private database: IDatabase,
@inject(TYPES.Logger) private logger: ILogger
) {}
createUser(name: string) {
this.logger.log('Creating user');
this.database.insert({ name });
}
}
// Регистрация в контейнере
const container = new Container();
container.bind(TYPES.Database).to(PostgresDatabase);
container.bind(TYPES.Logger).to(ConsoleLogger);
container.bind(TYPES.UserService).to(UserService);
// Получение экземпляра
const userService = container.get<UserService>(TYPES.UserService);
userService.createUser('John');
// Для тестов просто меняем регистрацию
const testContainer = new Container();
testContainer.bind(TYPES.Database).to(MockDatabase);
testContainer.bind(TYPES.Logger).to(MockLogger);
Пример с NestJS (использует InversifyJS под капотом):
// NestJS автоматически управляет DI
@Injectable()
class DatabaseService implements IDatabase {
insert(data: any) { /* реализация */ }
}
@Injectable()
class LoggerService implements ILogger {
log(message: string) { /* реализация */ }
}
@Injectable()
class UserService {
constructor(
private database: DatabaseService,
private logger: LoggerService
) {}
createUser(name: string) {
this.logger.log('Creating user');
this.database.insert({ name });
}
}
@Module({
providers: [DatabaseService, LoggerService, UserService],
exports: [UserService]
})
export class UserModule {}
// В контроллере
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Post()
async create(@Body() body: { name: string }) {
this.userService.createUser(body.name);
}
}
Преимущества DI
1. Тестируемость
describe('UserService', () => {
it('should create user', () => {
const mockDb = new MockDatabase();
const mockLogger = new MockLogger();
const service = new UserService(mockDb, mockLogger);
service.createUser('John');
// легко проверить, что методы вызваны
});
});
2. Слабая связанность
// Можно менять реализацию без изменения UserService
container.bind(TYPES.Database).to(MySQLDatabase); // было Postgres
container.bind(TYPES.Logger).to(FileLogger); // было Console
3. Переиспользование
// Один DatabaseService может использоваться многими сервисами
container.bind(TYPES.Database).to(PostgresDatabase).inSingletonScope();
// Все сервисы получат одно и то же значение
4. Явная зависимость
// Сразу видно, что нужно для работы сервиса
class UserService {
constructor(
private database: IDatabase, // явно
private logger: ILogger, // видно
private cache: ICacheService // какие зависимости
) {}
}
Недостатки
- Больше boilerplate кода
- Сложнее для начинающих
- Может быть медленнее при создании множества объектов
- Требует конфигурирования контейнера
Best Practices
- Инжектируйте интерфейсы, а не реализации
- Используйте constructor injection (самый явный)
- Регистрируйте зависимости один раз в контейнере
- Используйте singleton для stateless сервисов
- Создавайте новый контейнер для каждого теста