Какие практические проблемы решает внедрение зависимостей в Node.js?

«Какие практические проблемы решает внедрение зависимостей в Node.js?» — вопрос из категории Паттерны, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В моих Node.js проектах DI решает несколько ключевых проблем:

1. Тестируемость — мокирование зависимостей становится тривиальным:

// Без DI - сложно тестировать
class UserService {
  constructor() {
    this.db = require('./database'); // Прямая зависимость
  }
  async getUser(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

// С DI - легко тестировать
class UserService {
  constructor(db) {
    this.db = db; // Зависимость инжектится
  }
  async getUser(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

// В тесте
const mockDb = {
  query: jest.fn().mockResolvedValue({ id: 1, name: 'John' })
};
const service = new UserService(mockDb);
await service.getUser(1);
expect(mockDb.query).toHaveBeenCalled();

2. Гибкость конфигурации — можно переключать реализации без изменения кода:

// В development используем SQLite
if (process.env.NODE_ENV === 'development') {
  container.register('db', new SQLiteDatabase());
}
// В production - PostgreSQL
else {
  container.register('db', new PostgresDatabase(process.env.DATABASE_URL));
}

3. Управление жизненным циклом — DI-контейнеры вроде tsyringe или inversify позволяют контролировать создание инстансов:

import { singleton } from 'tsyringe';

@singleton() // Синглтон на всё приложение
class Logger {
  log(message: string) {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

4. Избегание циклических зависимостей — контейнер разрешает зависимости лениво, что помогает с circular dependencies в Node.js.

5. Чистая архитектура — зависимости явно объявлены в конструкторе, что делает код более читаемым и поддерживаемым.