Предыстория
В одном из моих проектов мы использовали библиотеку 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'; // Деклaрация модуля @Injectable() class AppService {} @NsModule({ providers: [AppService] }) class AppModule {} // Деклaрация модуля // Точка старта приложения 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/search?q=%40nexus-ioc
Github Wiki: https://github.com/Isqanderm/ioc/wiki
ссылка на оригинал статьи https://habr.com/ru/articles/853722/
Добавить комментарий