Предыстория
В одном из моих проектов мы использовали библиотеку Inversify для внедрения зависимостей (DI). Хотя это мощное и гибкое решение, его избыточная гибкость со временем обернулась против нас: управление зависимостями становилось всё более запутанным по мере роста приложения. С каждым новым модулем или компонентом код усложнялся, а процесс рефакторинга становился всё более болезненным.
Я выделил несколько ключевых требований, которые хотел бы видеть в новом решении:
-
Прозрачность зависимостей: Нужно было ясно понимать, какие зависимости требуются каждому компоненту, без лишней магии в коде.
-
Иерархичность: Важно было поддерживать строгую структуру, где модули и зависимости чётко организованы и легко управляемы.
-
Расширяемость: Код должен оставаться легко расширяемым без необходимости переписывать существующие части.
-
Раннее обнаружение ошибок: Ловить ошибки на этапе разработки, а не во время выполнения приложения.
После изучения популярных решений вроде Angular и NestJS, я понял, что эти фреймворки предлагают отличные возможности для управления зависимостями, но они слишком тесно интегрированы в свою экосистему, что затрудняет их применение вне этого контекста. Мне нужно было что-то универсальное. Так родилась идея Nexus-IoC — легковесного и гибкого инструмента для управления зависимостями в любых TypeScript-проектах.
Nexus-IoC
Для начала давайте рассмотрим пример простого приложения, чтобы познакомиться с библиотекой. Если вы уже работали с Angular или NestJS, этот код будет вам хорошо знаком.
import { NsModule, Injectable } from '@nexus-ioc/core';
import { NexusApplicationsBrowser } from '@nexus-ioc/core/dist/server';
// Деклорация модуля
@Injectable()
class AppService {}
@NsModule({
providers: [AppService]
})
class AppModule {}
// Деклорация модуля
// Точка старта приложения
async function bootstrap() {
const app = await NexusApplicationsBrowser
.create(AppModule)
.bootstrap();
}
bootstrap();
Основные концепции
-
Модульная архитектура
В Nexus-IoC вся логика приложения организована вокруг модулей — изолированных единиц кода, которые могут включать провайдеры (зависимости) и другие модули. Это помогает структурировать приложение и упростить управление зависимостями. -
Провайдеры и зависимости
Провайдеры — это объекты, которые могут быть внедрены в другие части приложения. В каждом модуле регистрируются свои провайдеры, и система автоматически разрешает зависимости между ними, что упрощает логику внедрения. -
Граф зависимостей
При запуске приложения Nexus-IoC автоматически строит граф зависимостей между модулями и провайдерами. Это помогает видеть, какие зависимости требуются каждому модулю, и находить ошибки на этапе сборки, такие как циклические зависимости или отсутствующие провайдеры. -
Асинхронная загрузка модулей
Nexus-IoC поддерживает асинхронную загрузку модулей, что помогает оптимизировать работу приложения. Только необходимые части кода загружаются в нужный момент, что особенно важно для производительности крупных приложений. -
Плагины для расширения функциональности
Система плагинов позволяет легко добавлять новые возможности, не изменяя основную библиотеку. Например, можно подключить плагины для визуализации графа зависимостей или для статического анализа кода.
Реализация модулей и провайдеров
Основная концепция Nexus-IoC — это модуль. С помощью декоратора @NsModule
, который принимает три ключевых параметра, вы можете объявить модуль:
-
imports
— список модулей, используемых внутри текущего. -
providers
— список провайдеров, предоставляемых этим модулем. -
exports
— список провайдеров, доступных для других модулей.
Типы провайдеров
-
UseClass провайдер — предоставляет класс для создания экземпляра зависимости.
{ provide: "classProvider", useClass: class ClassProvider {} }
-
Class провайдер — простой провайдер, который регистрирует класс.
@Injectable() class Provider {}
-
UseValue провайдер — предоставляет конкретное значение или объект.
{ provide: "value-token", useValue: 'value' }
-
UseFactory провайдер — позволяет создавать зависимости через фабричную функцию.
{ provide: "factory-token", useFactory: () => { // Поддерживается синхронный так и асинхронный вариант фабрики }, },
Проверка целостности графа зависимостей
Nexus-IoC проверяет целостность графа зависимостей ещё до запуска приложения. Подобно NestJS, библиотека анализирует граф зависимостей и выявляет такие проблемы, как циклические зависимости или отсутствующие провайдеры. Но в Nexus-IoC этот процесс более гибкий: граф строится с учётом ограничений между модулями, и фактические экземпляры зависимостей создаются только при их обращении.
Также Nexus-IoC предоставляет список ошибок, что позволяет заранее обнаруживать проблемы перед запуском приложения.
async function bootstrap() {
const app = await NexusApplicationsBrowser
.create(AppModule)
.bootstrap();
console.log(app.errors) // Здесь хранятся ошибки обнаруженные при построении графа
}
bootstrap();
Тестирование
Был реализован пакет @nexus-ioc/testing, который значительно облегчает процесс тестирования контейнеров и их компонентов. С ее помощью можно довольно легко писать unit тесты на модули и/или провайдеры.
import { Injectable } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';
describe('AppModule', () => {
it('should create AppService instance', async () => {
@Injectable()
class AppService {}
const appModule = await Test.createModule({
providers: [AppService],
}).compile();
const appService = await appModule.get<AppService>(AppService);
expect(appService).toBeInstanceOf(AppService);
});
});
Подмена зависимостей внутри сервиса
import { Injectable, Inject } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';
describe('AppModule', () => {
it('should create AppService instance', async () => {
const MOCK_SECRET_KEY = 'secret-key'
@Injectable()
class AppService {
constructor(@Inject('secret-key') public readonly secretKey: string) {}
}
const appModule = await Test.createModule({
providers: [AppService, { provide: 'secret-key', useValue: MOCK_SECRET_KEY }],
}).compile();
const appService = await appModule.get<AppService>(AppService);
expect(appService?.secretKey).toEqual(MOCK_SECRET_KEY);
});
});
Переиспользуемость
В Nexus-IoC реализованы знакомые методы для создания переиспользуемых модулей — forRoot
и forFeature
. Они позволяют гибко настраивать модули в зависимости от нужд приложения.
Отличия forRoot и forFeature
-
forRoot
: Эти методы регистрируют провайдеров на глобальном уровне. Они особенно полезны для сервисов, которые должны быть доступны в любом модуле приложения. -
forFeature
: Эти методы регистрируют провайдеров только в пределах текущего модуля, что делает их идеальными для локальных или специализированных сервисов.
Пример использования
Вы можете использовать forRoot
, чтобы зарегистрировать глобальные сервисы, такие как логирование, и forFeature
для локальных обработчиков, которые нужны только в конкретных модулях.
Пример forRoot модуля
import { NsModule, Injectable, DynamicModule } from '@nexus-ioc/core';
interface ConfigOptions {
apiUrl: string;
}
// Сервис настроек
@Injectable()
class ConfigService {
async getOptions(): Promise<ConfigOptions> {
// симулируем загрузку данных из API
return new Promise((resolve) => {
setTimeout(() => resolve({ apiUrl: 'https://api.async.example.com' }), 1000);
});
}
}
@NsModule()
export class ConfigModule {
// Обьявления модуля для глобального инстанцирования
static forRoot(): DynamicModule {
return {
module: ConfigModule,
providers: [
ConfigService,
{
provide: 'CONFIG_OPTIONS',
// Поддерживаются синхронные и асинхронные обьявления фабрик
useFactory: (configService: ConfigService) =>
configService.getOptions(),
inject: [ConfigService], // Описываем зависимости фабрики
},
],
exports: ['CONFIG_OPTIONS'],
};
}
// Обьявление модуля для локального инстанцирования
static forFeature(): DynamicModule {
return {
module: ConfigModule,
providers: [
ConfigService,
{
provide: 'CONFIG_OPTIONS',
useFactory: (configService: ConfigService) =>
configService.getOptions(),
inject: [ConfigService], // Описываем зависимости фабрики
},
],
exports: ['CONFIG_OPTIONS'],
};
}
}
Плагины для расширения функциональности
Одной из важных особенностей Nexus-IoC является возможность расширять функциональность с помощью плагинов. Они позволяют добавлять новые возможности без изменения основного кода библиотеки.
Один из примеров — это интеграция с инструмента для анализа и визуализации графа зависимостей.
Для этого Nexus-IoC предоставляет метод addScannerPlugin
, с помощью которого можно подключать плагины на этапе сканирования графа зависимостей. Этот метод позволяет интегрировать сторонние инструменты, которые могут взаимодействовать с графом во время его построения.
Как работает addScannerPlugin
Метод addScannerPlugin
принимает плагин в виде функции, которая будет вызываться после этапа построения графа зависимостей. Плагин получает информацию о графе, его узлах и ребрах. Можно реализовать доп. проверки или модифицировать граф.
Первый плагин, который был создан - это GraphScannerVisualizer
. Его задача в том, чтобы визуализировать граф.
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
import { GraphScannerVisualizer } from 'nexus-ioc-graph-visualizer';
import { AppModule } from './apps';
// Добавляем плагин визуализации
const visualizer = new GraphScannerVisualizer("./graph.png");
async function bootstrap() {
await NexusApplicationsServer.create(AppModule)
.addScannerPlugin(visualizer)
.bootstrap();
}
bootstrap();
Сравнение с другими вариантами
Пример на Nexus-IoC
import { Injectable, NsModule, Scope } from '@nexus-ioc/core';
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
@Injectable({ scope: Scope.Singleton })
class LoggerService {
log(message: string) {
console.log(message);
}
}
@Injectable()
class UserService {
constructor(private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`logger: ${userId}`);
}
}
@NsModule({
providers: [LoggerService, UserService],
})
class AppModule {}
async function bootstrap() {
const container = new NexusApplicationsServe.create(AppModule).bootstrap();
const userService = await container.get<UserService>(UserService);
userService.printUser('log me!');
}
bootstrap();
пример на inversify
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';
@injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@injectable()
class UserService {
constructor(@inject(LoggerService) private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`User ID: ${userId}`);
}
}
const container = new Container();
container.bind(LoggerService).toSelf();
container.bind(UserService).toSelf();
const userService = container.get(UserService);
userService.printUser('123');
пример на Tsyringe:
import 'reflect-metadata';
import { container, injectable } from 'tsyringe';
@injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@injectable()
class UserService {
constructor(private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`User ID: ${userId}`);
}
}
container.registerSingleton(LoggerService);
container.registerSingleton(UserService);
const userService = container.resolve(UserService);
userService.printUser('123');
Как вы видите, тут нет какой-то вундервафли, которая бы меняла правила игры и уничтожала конкурентов, библиотека управляет зависимостями, просто чуть-чуть делая это по другому. Главное отличие от других решений - это декларативное объявление модулей открывает большие возможности для статического анализа кода, что помогает при разработке больших приложений.
Напоследок
Кому пригодится данное решение: Nexus-IoC особенно хорошо подходит для крупных приложений (enterprise уровня), где важно не только управление зависимостями, но и ясность структуры приложения. Я бы не рекомендовал это решение для маленьких и средних приложений — здесь вы вполне сможете обойтись без DI, особенно на начальных этапах. Однако, когда проект становится масштабным, с десятками разработчиков и командами, взаимодействующими через контракты, Nexus-IoC может снять множество проблем, связанных с управлением зависимостями, предоставив при этом мощные инструменты для поддержки и анализа кода.
В планах:
-
API уже стабилен и меняться не будет, но еще предстоит работа по оптимизации и полному покрытию тестами, чтобы довести библиотеку до версии 1.0
-
Разработка CLI для упрощения работы с библиотекой
-
Создание статического анализатора графа зависимостей, чтобы выявлять ошибки ещё до этапа сборки
-
Разработка плагинов для IDE для улучшения интеграции с редакторами
-
Улучшение документации и создания сайта для удобства разработчиков
Ссылка на репозиторий: https://github.com/Isqanderm/ioc
Ссылка на npm пакеты: https://www.npmjs.com/settings/nexus-ioc/packages
Github Wiki: https://github.com/Isqanderm/ioc/wiki
Nexus-IoC
Для начала давайте рассмотрим пример простого приложения, чтобы познакомиться с библиотекой. Если вы уже работали с Angular или NestJS, этот код будет вам хорошо знаком.
import { NsModule, Injectable } from '@nexus-ioc/core';
import { NexusApplicationsBrowser } from '@nexus-ioc/core/dist/server';
// Деклорация модуля
@Injectable()
class AppService {}
@NsModule({
providers: [AppService]
})
class AppModule {}
// Деклорация модуля
// Точка старта приложения
async function bootstrap() {
const app = await NexusApplicationsBrowser
.create(AppModule)
.bootstrap();
}
bootstrap();
Основные концепции
-
Модульная архитектура
В Nexus-IoC вся логика приложения организована вокруг модулей — изолированных единиц кода, которые могут включать провайдеры (зависимости) и другие модули. Это помогает структурировать приложение и упростить управление зависимостями. -
Провайдеры и зависимости
Провайдеры — это объекты, которые могут быть внедрены в другие части приложения. В каждом модуле регистрируются свои провайдеры, и система автоматически разрешает зависимости между ними, что упрощает логику внедрения. -
Граф зависимостей
При запуске приложения Nexus-IoC автоматически строит граф зависимостей между модулями и провайдерами. Это помогает видеть, какие зависимости требуются каждому модулю, и находить ошибки на этапе сборки, такие как циклические зависимости или отсутствующие провайдеры. -
Асинхронная загрузка модулей
Nexus-IoC поддерживает асинхронную загрузку модулей, что помогает оптимизировать работу приложения. Только необходимые части кода загружаются в нужный момент, что особенно важно для производительности крупных приложений. -
Плагины для расширения функциональности
Система плагинов позволяет легко добавлять новые возможности, не изменяя основную библиотеку. Например, можно подключить плагины для визуализации графа зависимостей или для статического анализа кода.
Реализация модулей и провайдеров
Основная концепция Nexus-IoC — это модуль. С помощью декоратора @NsModule
, который принимает три ключевых параметра, вы можете объявить модуль:
-
imports
— список модулей, используемых внутри текущего. -
providers
— список провайдеров, предоставляемых этим модулем. -
exports
— список провайдеров, доступных для других модулей.
Типы провайдеров
-
UseClass провайдер — предоставляет класс для создания экземпляра зависимости.
{ provide: "classProvider", useClass: class ClassProvider {} }
-
Class провайдер — простой провайдер, который регистрирует класс.
@Injectable() class Provider {}
-
UseValue провайдер — предоставляет конкретное значение или объект.
{ provide: "value-token", useValue: 'value' }
-
UseFactory провайдер — позволяет создавать зависимости через фабричную функцию.
{ provide: "factory-token", useFactory: () => { // Поддерживается синхронный так и асинхронный вариант фабрики }, },
Проверка целостности графа зависимостей
Nexus-IoC проверяет целостность графа зависимостей ещё до запуска приложения. Подобно NestJS, библиотека анализирует граф зависимостей и выявляет такие проблемы, как циклические зависимости или отсутствующие провайдеры. Но в Nexus-IoC этот процесс более гибкий: граф строится с учётом ограничений между модулями, и фактические экземпляры зависимостей создаются только при их обращении.
Также Nexus-IoC предоставляет список ошибок, что позволяет заранее обнаруживать проблемы перед запуском приложения.
async function bootstrap() {
const app = await NexusApplicationsBrowser
.create(AppModule)
.bootstrap();
console.log(app.errors) // Здесь хранятся ошибки обнаруженные при построении графа
}
bootstrap();
Тестирование
Был реализован пакет @nexus-ioc/testing, который значительно облегчает процесс тестирования контейнеров и их компонентов. С ее помощью можно довольно легко писать unit тесты на модули и/или провайдеры.
import { Injectable } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';
describe('AppModule', () => {
it('should create AppService instance', async () => {
@Injectable()
class AppService {}
const appModule = await Test.createModule({
providers: [AppService],
}).compile();
const appService = await appModule.get<AppService>(AppService);
expect(appService).toBeInstanceOf(AppService);
});
});
Подмена зависимостей внутри сервиса
import { Injectable, Inject } from '@nexus-ioc/core';
import { Test } from '@nexus-ioc/testing';
describe('AppModule', () => {
it('should create AppService instance', async () => {
const MOCK_SECRET_KEY = 'secret-key'
@Injectable()
class AppService {
constructor(@Inject('secret-key') public readonly secretKey: string) {}
}
const appModule = await Test.createModule({
providers: [AppService, { provide: 'secret-key', useValue: MOCK_SECRET_KEY }],
}).compile();
const appService = await appModule.get<AppService>(AppService);
expect(appService?.secretKey).toEqual(MOCK_SECRET_KEY);
});
});
Переиспользуемость
В Nexus-IoC реализованы знакомые методы для создания переиспользуемых модулей — forRoot
и forFeature
. Они позволяют гибко настраивать модули в зависимости от нужд приложения.
Отличия forRoot и forFeature
-
forRoot
: Эти методы регистрируют провайдеров на глобальном уровне. Они особенно полезны для сервисов, которые должны быть доступны в любом модуле приложения. -
forFeature
: Эти методы регистрируют провайдеров только в пределах текущего модуля, что делает их идеальными для локальных или специализированных сервисов.
Пример использования
Вы можете использовать forRoot
, чтобы зарегистрировать глобальные сервисы, такие как логирование, и forFeature
для локальных обработчиков, которые нужны только в конкретных модулях.
Пример forRoot модуля
import { NsModule, Injectable, DynamicModule } from '@nexus-ioc/core';
interface ConfigOptions {
apiUrl: string;
}
// Сервис настроек
@Injectable()
class ConfigService {
async getOptions(): Promise<ConfigOptions> {
// симулируем загрузку данных из API
return new Promise((resolve) => {
setTimeout(() => resolve({ apiUrl: 'https://api.async.example.com' }), 1000);
});
}
}
@NsModule()
export class ConfigModule {
// Обьявления модуля для глобального инстанцирования
static forRoot(): DynamicModule {
return {
module: ConfigModule,
providers: [
ConfigService,
{
provide: 'CONFIG_OPTIONS',
// Поддерживаются синхронные и асинхронные обьявления фабрик
useFactory: (configService: ConfigService) =>
configService.getOptions(),
inject: [ConfigService], // Описываем зависимости фабрики
},
],
exports: ['CONFIG_OPTIONS'],
};
}
// Обьявление модуля для локального инстанцирования
static forFeature(): DynamicModule {
return {
module: ConfigModule,
providers: [
ConfigService,
{
provide: 'CONFIG_OPTIONS',
useFactory: (configService: ConfigService) =>
configService.getOptions(),
inject: [ConfigService], // Описываем зависимости фабрики
},
],
exports: ['CONFIG_OPTIONS'],
};
}
}
Плагины для расширения функциональности
Одной из важных особенностей Nexus-IoC является возможность расширять функциональность с помощью плагинов. Они позволяют добавлять новые возможности без изменения основного кода библиотеки.
Один из примеров — это интеграция с инструмента для анализа и визуализации графа зависимостей.
Для этого Nexus-IoC предоставляет метод addScannerPlugin
, с помощью которого можно подключать плагины на этапе сканирования графа зависимостей. Этот метод позволяет интегрировать сторонние инструменты, которые могут взаимодействовать с графом во время его построения.
Как работает addScannerPlugin
Метод addScannerPlugin
принимает плагин в виде функции, которая будет вызываться после этапа построения графа зависимостей. Плагин получает информацию о графе, его узлах и ребрах. Можно реализовать доп. проверки или модифицировать граф.
Первый плагин, который был создан - это GraphScannerVisualizer
. Его задача в том, чтобы визуализировать граф.
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
import { GraphScannerVisualizer } from 'nexus-ioc-graph-visualizer';
import { AppModule } from './apps';
// Добавляем плагин визуализации
const visualizer = new GraphScannerVisualizer("./graph.png");
async function bootstrap() {
await NexusApplicationsServer.create(AppModule)
.addScannerPlugin(visualizer)
.bootstrap();
}
bootstrap();
Сравнение с другими вариантами
Пример на Nexus-IoC
import { Injectable, NsModule, Scope } from '@nexus-ioc/core';
import { NexusApplicationsServer } from '@nexus-ioc/core/dist/server';
@Injectable({ scope: Scope.Singleton })
class LoggerService {
log(message: string) {
console.log(message);
}
}
@Injectable()
class UserService {
constructor(private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`logger: ${userId}`);
}
}
@NsModule({
providers: [LoggerService, UserService],
})
class AppModule {}
async function bootstrap() {
const container = new NexusApplicationsServe.create(AppModule).bootstrap();
const userService = await container.get<UserService>(UserService);
userService.printUser('log me!');
}
bootstrap();
пример на inversify
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';
@injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@injectable()
class UserService {
constructor(@inject(LoggerService) private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`User ID: ${userId}`);
}
}
const container = new Container();
container.bind(LoggerService).toSelf();
container.bind(UserService).toSelf();
const userService = container.get(UserService);
userService.printUser('123');
пример на Tsyringe:
import 'reflect-metadata';
import { container, injectable } from 'tsyringe';
@injectable()
class LoggerService {
log(message: string) {
console.log(message);
}
}
@injectable()
class UserService {
constructor(private logger: LoggerService) {}
printUser(userId: string) {
this.logger.log(`User ID: ${userId}`);
}
}
container.registerSingleton(LoggerService);
container.registerSingleton(UserService);
const userService = container.resolve(UserService);
userService.printUser('123');
Как вы видите, тут нет какой-то вундервафли, которая бы меняла правила игры и уничтожала конкурентов, библиотека управляет зависимостями, просто чуть-чуть делая это по другому. Главное отличие от других решений - это декларативное объявление модулей открывает большие возможности для статического анализа кода, что помогает при разработке больших приложений.
Напоследок
Кому пригодится данное решение: Nexus-IoC особенно хорошо подходит для крупных приложений (enterprise уровня), где важно не только управление зависимостями, но и ясность структуры приложения. Я бы не рекомендовал это решение для маленьких и средних приложений — здесь вы вполне сможете обойтись без DI, особенно на начальных этапах. Однако, когда проект становится масштабным, с десятками разработчиков и командами, взаимодействующими через контракты, Nexus-IoC может снять множество проблем, связанных с управлением зависимостями, предоставив при этом мощные инструменты для поддержки и анализа кода.
В планах:
-
API уже стабилен и меняться не будет, но еще предстоит работа по оптимизации и полному покрытию тестами, чтобы довести библиотеку до версии 1.0
-
Разработка CLI для упрощения работы с библиотекой
-
Создание статического анализатора графа зависимостей, чтобы выявлять ошибки ещё до этапа сборки
-
Разработка плагинов для IDE для улучшения интеграции с редакторами
-
Улучшение документации и создания сайта для удобства разработчиков
Ссылка на репозиторий: https://github.com/Isqanderm/ioc
Ссылка на npm пакеты: https://www.npmjs.com/settings/nexus-ioc/packages
Github Wiki: https://github.com/Isqanderm/ioc/wiki
Автор: AlexEOL