Dependency Injection контейнер для JavaScript (Node.js) — Awilix

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

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

Начать курс

Dependency Injection контейнер для JavaScript (Node.js) — Awilix

В современной разработке на Node.js чистота архитектуры и тестируемость кода выходят на первый план. Одним из ключевых инструментов для достижения этих целей является внедрение зависимостей (Dependency Injection, DI). Библиотека Awilix предоставляет мощный, производительный и гибкий DI-контейнер для JavaScript и TypeScript, идеально подходящий для серверных приложений на Node.js. В этой статье мы подробно разберём, как использовать Awilix для управления зависимостями, улучшения модульности и упрощения тестирования.

Что такое Dependency Injection и зачем нужен контейнер?

Dependency Injection — это паттерн проектирования, при котором объект получает свои зависимости извне, а не создаёт их сам. Это реализует принцип Inversion of Control (IoC). Контейнер — это инструмент, который автоматизирует процесс создания и предоставления этих зависимостей. Вместо ручного создания экземпляров классов и передачи их друг другу, вы описываете правила сборки в контейнере, и он сам разрешает все цепочки зависимостей.

Основные преимущества использования DI-контейнера:

  • Снижение связанности кода: Модули зависят от абстракций, а не от конкретных реализаций.
  • Упрощение тестирования: Легко подменять реальные сервисы на моки (mock) во время unit-тестов.
  • Централизованное управление: Вся конфигурация зависимостей находится в одном месте, что упрощает рефакторинг.
  • Управление жизненным циклом: Контейнер может контролировать, как долго живёт объект — один экземпляр на всё приложение (синглтон) или новый при каждом запросе.

Установка Awilix

Для начала работы установите библиотеку через npm:

npm install awilix

Awilix не имеет внешних зависимостей и работает как с CommonJS, так и с ESM модулями.

Основные концепции Awilix

Awilix строится вокруг нескольких ключевых понятий:

  • Container (контейнер): Основной объект, который хранит и предоставляет зависимости.
  • Registration (регистрация): Процесс связывания имени (токена) с фабрикой, классом или значением.
  • Resolver (резолвер): Объект, который определяет, как именно создавать или получать зависимость. Например, asClass(), asFunction(), asValue().
  • Lifetime (время жизни): Указывает, сколько раз будет создан объект: ScopedLifeTime (один раз на скоуп), SingletonLifeTime (один раз за всё время) или TransientLifeTime (каждый раз новый).

Пример использования: сервис для работы с пользователями

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

1. Определяем классы

// database.js
class Database {
    constructor() {
        this.connection = 'Connected to DB';
    }
    query(sql) {
        return `Result for: ${sql}`;
    }
}

// userRepository.js
class UserRepository {
    constructor({ database }) {
        this.db = database;
    }
    findById(id) {
        return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
    }
}

// userService.js
class UserService {
    constructor({ userRepository }) {
        this.repo = userRepository;
    }
    getUser(id) {
        return this.repo.findById(id);
    }
}

Обратите внимание на конструкторы: они принимают объект с зависимостями. Это стандартная практика для Awilix.

2. Настройка контейнера

const { createContainer, asClass, asValue, Lifetime } = require('awilix');

const container = createContainer();

// Регистрируем зависимости
container.register({
    database: asClass(Database).singleton(),
    userRepository: asClass(UserRepository).scoped(),
    userService: asClass(UserService).scoped()
});

// Или можно использовать цепочку методов:
// container.register('database', asClass(Database).setLifetime(Lifetime.SINGLETON));

Здесь asClass(Database).singleton() означает, что объект базы данных будет создан один раз. scoped() создаёт новый экземпляр для каждого скоупа (например, для каждого HTTP-запроса).

3. Использование контейнера

// Создаём скоуп (например, на каждый запрос)
const scope = container.createScope();

// Получаем сервис
const userService = scope.resolve('userService');
console.log(userService.getUser(1)); // Result for: SELECT * FROM users WHERE                    

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