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