Dependency Injection в JavaScript: полное руководство для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Что такое Dependency Injection и зачем он нужен?

Представьте, что вы собираете компьютер. Вместо того чтобы покупать готовый моноблок, вы выбираете отдельно процессор, видеокарту, оперативную память и подключаете их к материнской плате. Если вы захотите обновить видеокарту, вам не придётся менять весь компьютер — вы просто замените один компонент. Dependency Injection (DI) в программировании работает по тому же принципу.

Dependency Injection (внедрение зависимостей) — это паттерн проектирования, при котором объект получает свои зависимости извне, а не создаёт их самостоятельно. В контексте JavaScript и Node.js это означает, что класс или функция не создают экземпляры других классов внутри себя, а принимают их через конструктор или параметры.

Этот подход является частью принципа Инверсии зависимостей (Dependency Inversion Principle) из SOLID и делает код более гибким, тестируемым и поддерживаемым. В этой статье мы разберём DI на практических примерах для начинающих разработчиков.

Проблема: жёсткая связность кода

Рассмотрим типичную ситуацию в Node.js приложении. Допустим, у нас есть сервис для отправки email-уведомлений:

// Без Dependency Injection
class EmailService {
  sendEmail(to, subject, body) {
    console.log(`Отправка email на ${to}: ${subject}`);
    // реальная логика отправки
  }
}

class UserService {
  constructor() {
    this.emailService = new EmailService(); // ❌ Жёсткая связность
  }

  registerUser(email) {
    // Логика регистрации пользователя
    this.emailService.sendEmail(email, 'Добро пожаловать!', '...');
  }
}

const userService = new UserService();
userService.registerUser('user@example.com');

В чём проблема? Класс UserService жёстко привязан к конкретной реализации EmailService. Если вы захотите:

  • Заменить отправку email на отправку SMS
  • Добавить логирование всех отправленных сообщений
  • Протестировать UserService без реальной отправки писем

Вам придётся переписывать код UserService. Это нарушает принцип открытости/закрытости (Open/Closed Principle).

Решение: внедрение зависимостей через конструктор

Первый и самый простой способ внедрения зависимостей — передача их через конструктор класса. Давайте перепишем наш пример:

// С Dependency Injection
class EmailService {
  sendEmail(to, subject, body) {
    console.log(`Отправка email на ${to}: ${subject}`);
  }
}

class SmsService {
  sendSms(phone, message) {
    console.log(`Отправка SMS на ${phone}: ${message}`);
  }
}

class UserService {
  constructor(notificationService) {
    this.notificationService = notificationService; // ✅ Внедрение зависимости
  }

  registerUser(contact, isPhone = false) {
    // Логика регистрации
    if (isPhone) {
      this.notificationService.sendSms(contact, 'Добро пожаловать!');
    } else {
      this.notificationService.sendEmail(contact, 'Добро пожаловать!', '...');
    }
  }
}

// Использование
const emailService = new EmailService();
const userServiceWithEmail = new UserService(emailService);
userServiceWithEmail.registerUser('user@example.com');

const smsService = new SmsService();
const userServiceWithSms = new UserService(smsService);
userServiceWithSms.registerUser('+1234567890', true);

Теперь UserService не знает, какой именно сервис используется — email или SMS. Он просто вызывает метод sendEmail или sendSms у переданного объекта. Это называется инверсией зависимостей: высокоуровневый модуль не зависит от низкоуровневого, оба зависят от абстракции.

Преимущества этого подхода:

  • Гибкость: легко заменить одну реализацию на другую
  • Тестируемость: можно передать mock-объект для тестов
  • Переиспользуемость: сервисы можно использовать в разных контекстах

Продвинутый уровень: DI-контейнер в Node.js

Когда приложение разрастается, ручное создание и передача всех зависимостей становится утомительным. На помощь приходит DI-контейнер (Inversion of Control контейнер). Это инструмент, который автоматически создаёт и внедряет зависимости.

Рассмотрим простой DI-контейнер для Node.js, написанный с нуля:

// Простой DI-контейнер
class DIContainer {
  constructor() {
    this.services = {};
    this.singletons = {};
  }

  register(name, factory, isSingleton = false) {
    this.services[name] = { factory, isSingleton };
  }

  get(name) {
    const service = this.services[name];
    if (!service) {
      throw new Error(`Сервис "${name}" не найден`);
    }

    if (service.isSingleton) {
      if (!this.singletons[name]) {
        this.singletons[name] = service.factory(this);
      }
      return this.singletons[name];
    }

    return service.factory(this);
  }
}

// Определяем сервисы
const container = new DIContainer();

container.register('logger', () => ({
  log: (msg) => console.log(`[LOG]: ${msg}`),
  error: (msg) => console.error(`[ERROR]: ${msg}`)
}), true); // singleton

container.register('emailService', (c) => ({
  send: (to, subject) => {
    c.get('logger').log(`Отправка email на ${to}: ${subject}`);
    // реальная логика отправки
  }
}), false);

container.register('userService', (c) => ({
  register: (email) => {
    // Логика регистрации
    c.get('emailService').send(email, 'Добро пожаловать!');
    c.get('logger').log(`Пользователь ${email} зарегистрирован`);
  }
}), false);

// Использование
const userService = container.get('userService');
userService.register('user@example.com');
// Вывод:
// [LOG]: Отправка email на user@example.com: Добро пожаловать!
// [LOG]: Пользователь user@example.com зарегистрирован

На практике для Node.js часто используют готовые библиотеки, такие как Awilix или tsyringe. Вот пример с Awilix:

// Установка: npm install awilix
const { createContainer, asClass, asValue, asFunction } = require('awilix');

class Logger {
  log(msg) { console.log(msg); }
}

class EmailService {
  constructor({ logger }) {
    this.logger = logger;
  }
  send(to, subject) {
    this.logger.log(`Email отправлен на ${to}`);
  }
}

class UserService {
  constructor({ emailService, logger }) {                

Блоги

Рекомендаций по книгам