Что такое Inversify и зачем он нужен?
Inversify — это мощный и легковесный IoC (Inversion of Control) контейнер для TypeScript и JavaScript. Он реализует паттерн Dependency Injection (DI), который является одним из ключевых принципов SOLID. Inversify позволяет управлять зависимостями между классами, делая код более модульным, тестируемым и поддерживаемым.
Зачем это нужно? В традиционном программировании классы сами создают свои зависимости (например, через new). Это приводит к сильной связанности кода. Inversify переворачивает этот процесс: зависимости "внедряются" извне (отсюда и название — Inversion of Control). Контейнер берет на себя создание и управление жизненным циклом объектов.
Основные преимущества использования Inversify:
- Слабая связанность: Классы зависят от абстракций (интерфейсов), а не от конкретных реализаций.
- Легкое тестирование: Вы можете легко подменять реальные зависимости на моки/стабы в тестах.
- Управление жизненным циклом: Контейнер может создавать объекты как синглтоны, так и каждый раз новые экземпляры.
- Читаемость и поддерживаемость: Все зависимости явно объявлены и легко отслеживаются.
- Масштабируемость: Отлично подходит для больших проектов с множеством модулей.
Установка Inversify
Inversify устанавливается через npm. Для работы с TypeScript также потребуется установить reflect-metadata (библиотека для работы с декораторами) и настроить tsconfig.json.
# Установка Inversify и reflect-metadata
npm install inversify reflect-metadata --save
# Установка типов для TypeScript (если не установлены)
npm install @types/node --save-dev
Настройка tsconfig.json:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["reflect-metadata"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Важно: Декораторы @injectable() и @inject() работают только при включенных experimentalDecorators и emitDecoratorMetadata.
Быстрый старт с примером кода
Создадим простой пример: сервис для отправки уведомлений. У нас будет интерфейс INotificationService, его реализация EmailNotificationService и класс UserService, который использует этот сервис.
// 1. Импортируем необходимые модули
import "reflect-metadata";
import { Container, injectable, inject } from "inversify";
// 2. Определяем интерфейс (абстракцию)
interface INotificationService {
send(message: string): void;
}
// 3. Реализация интерфейса
@injectable()
class EmailNotificationService implements INotificationService {
send(message: string): void {
console.log(`Отправка email: ${message}`);
}
}
// 4. Класс, который зависит от INotificationService
@injectable()
class UserService {
private notificationService: INotificationService;
constructor(
@inject("INotificationService") notificationService: INotificationService
) {
this.notificationService = notificationService;
}
notifyUser(userId: string): void {
this.notificationService.send(`Уведомление для пользователя ${userId}`);
}
}
// 5. Создаем контейнер и регистрируем зависимости
const container = new Container();
container.bind("INotificationService").to(EmailNotificationService);
container.bind(UserService).toSelf();
// 6. Получаем экземпляр UserService из контейнера
const userService = container.get(UserService);
userService.notifyUser("12345"); // Вывод: Отправка email: Уведомление для пользователя 12345
Основные методы/функции Inversify
1. Container — Конструктор контейнера
Что делает: Создает новый экземпляр IoC контейнера. Контейнер — это центральное хранилище всех зарегистрированных зависимостей.
Параметры: Принимает опциональный объект ContainerOptions (например, defaultScope для установки скоупа по умолчанию).
Пример:
import { Container } from "inversify";
const container = new Container();
// Или с настройками:
const containerWithOptions = new Container({ defaultScope: "Singleton" });
2. container.bind() — Регистрация зависимости
Что делает: Регистрирует привязку (binding) между идентификатором (символом, строкой или классом) и реализацией.
Параметры: Принимает идентификатор (ServiceIdentifier). Возвращает объект BindingToSyntax для дальнейшей настройки.
Пример:
container.bind<INotificationService>("INotificationService").to(EmailNotificationService);
container.bind<UserService>(UserService).toSelf();
3. container.get() — Получение экземпляра
Что делает: Разрешает зависимость и возвращает экземпляр зарегистрированного класса.
Параметры: Принимает идентификатор (ServiceIdentifier).
Пример:
const userService = container.get<UserService>(UserService);
const notificationService = container.get<INotificationService>("INotificationService");
4. container.getAll() — Получение всех экземпляров
Что делает: Возвращает массив всех зарегистрированных экземпляров для указанного идентификатора. Полезно, когда зарегистрировано несколько реализаций одного интерфейса.
Параметры: Принимает идентификатор (ServiceIdentifier).
Пример:
// Регистрируем несколько реализаций
container.bind<INotificationService>("INotificationService").to(EmailNotificationService);
container.bind<INotificationService>("INotificationService").to(SmsNotificationService);
// Получаем все
const services = container.getAll<INotificationService>("INotificationService");
services.forEach(service => service.send("Тестовое сообщение"));
5. container.isBound() — Проверка регистрации
Что делает: Проверяет, зарегистрирована ли зависимость с указанным идентификатором.
Параметры: Принимает идентификатор (ServiceIdentifier). Возвращает boolean.
Пример:
if (container.isBound("INotificationService")) {
const service = container.get<INotificationService>("INotificationService");
service.send("Проверка пройдена");
}
6. container.unbind() — Удаление регистрации
Что делает: Удаляет все привязки для указанного идентификатора.
Параметры: Принимает идентификатор (ServiceIdentifier).
Пример:
container.unbind("INotificationService");
// Теперь container.isBound("INotificationService") вернет false
7. container.rebind() — Перерегистрация зависимости
Что делает: Удаляет все существующие привязки для идентификатора и создает новую. Удобно для переопределения зависимостей (например, в тестах).
Параметры: Принимает идентификатор (ServiceIdentifier). Возвращает BindingToSyntax.
Пример:
// Переопределяем реализацию
container.rebind<INotificationService>("INotificationService").to(MockNotificationService);
8. container.snapshot() и container.restore() — Снимки состояния
Что делает: snapshot() сохраняет текущее состояние контейнера (все привязки). restore() восстанавливает последнее сохраненное состояние. Полезно для тестирования.
Параметры: Нет.
Пример:
// Сохраняем состояние
container.snapshot();
// Вносим изменения для теста
container.rebind<INotificationService>("INotificationService").to(MockNotificationService);
// После теста восстанавливаем
container.restore();
9. @injectable() — Декоратор класса
Что делает: Помечает класс как "инжектируемый". Без этого декоратора Inversify не сможет управлять зависимостями класса.
Параметры: Нет (применяется к классу).
Пример:
@injectable()
class MyService {
// ...
}
10. @inject() — Декоратор параметра
Что делает: Указывает, какую зависимость нужно внедрить в параметр конструктора или свойства класса.
Параметры: Принимает идентификатор (ServiceIdentifier).
Пример:
@injectable()
class UserService {
constructor(
@inject("INotificationService") private notificationService: INotificationService
) {}
}
11. container.applyMiddleware() — Применение middleware
Что делает: Позволяет добавить middleware (промежуточное ПО) для перехвата и модификации процесса разрешения зависимостей.
Параметры: Принимает одну или несколько функций middleware.
Пример:
// Middleware для логирования
const loggingMiddleware: Middleware = (planAndResolve) => {
return (args) => {
console.log(`Разрешение зависимости: ${args.serviceIdentifier}`);
return planAndResolve(args);
};
};
container.applyMiddleware(loggingMiddleware);
Реальный практический пример использования
Рассмотрим более сложный пример: приложение для управления задачами (Task Manager). У нас будут сервисы для работы с базой данных, логирования и уведомлений.
import "reflect-metadata";
import { Container, injectable, inject } from "inversify";
// 1. Определяем интерфейсы
interface ILogger {
log(message: string): void;
}
interface IDatabase {
save(task: string): void;
getAll(): string[];
}
interface INotificationService {
send(message: string): void;
}
// 2. Реализации
@injectable()
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
}
}
@injectable()
class InMemoryDatabase implements IDatabase {
private tasks: string[] = [];
save(task: string): void {
this.tasks.push(task);
}
getAll(): string[] {
return this.tasks;
}
}
@injectable()
class EmailNotificationService implements INotificationService {
send(message: string): void {
console.log(`[EMAIL] ${message}`);
}
}
// 3. Основной сервис управления задачами
@injectable()
class TaskManager {
constructor(
@inject("ILogger") private logger: