← Назад к вопросам

Как работает принцип 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

  1. Инжектируйте интерфейсы, а не реализации
  2. Используйте constructor injection (самый явный)
  3. Регистрируйте зависимости один раз в контейнере
  4. Используйте singleton для stateless сервисов
  5. Создавайте новый контейнер для каждого теста