Контейнер DI

Контейнер DI управляет зависимостями. В контейнере регистрируются зависимости всех программных решений приложения: модулей, компонентов, сервисов, ресурсов — любых объектов. Через контейнер предоставляется доступ ко всем программным решениям приложения. Контейнер берёт на себя обязанность создавать и инициализировать решение по первому требованию. Контейнер запоминает подготовленное решение, чтобы не создавать и не инициализировать его повторно при очередном запросе решения. Контейнер реализует паттерн "Одиночка".

Добавление регистрации set(inject)

В метод set передаётся регистрация одного из типов InjectFactory, InjectClass, InjectValue. Или массив регистраций.

set<Type, ExtType extends Type, Deps>(inject: InjectFactory<Type, ExtType, Deps>): this;  
set<Type, ExtType extends Type, Deps>(inject: InjectClass<Type, ExtType, Deps>): this;  
set<Type, ExtType extends Type>(inject: InjectValue<Type, ExtType>): this;  
set(inject: InjectArray): this;

Можно передать заранее подготовленную регистрацию или описать её непосредственно в методе.

container.set({
  token: FIRST,
  depends: { logger: LOGS, config: FIRST_CFG },
  constructor: First
})

Чтобы не вызывать метод set() для каждой добавляемой регистрации, можно передать все регистрации в массиве. Но регистрации тогда должны быть заранее подготовлены (иначе не будут корректно выводиться типы зависимостей)!

container.set([configs, renderService, routerService, modalsService, httpClient, i18nService, logService])

Если регистрация не подготовлена, но её нужно передать в массив, то можно воспользоваться утилитой injectFactory(), injectClass() или injectValue() для удобной типизации. То есть всё равно нужно подготовить регистрацию, но сделать это можно в момент добавления.

container.set([
  configs, 
  renderService,
  //... 
  injectClass({
    token: FIRST,
    depends: { logger: LOGS, config: FIRST_CFG },
    constructor: First
  })
])

Весь массив регистраций можно подготовить заранее. Например, в неком модуле приложения можно определить массив со всеми регистрациями на сервисы, компоненты, АПИ и ресурсы модуля.

// Регистарция модуля со всеми его составными частями (тоже регистрация)
export const catalogFeature = [  
  articlesApi,  
  categoriesApi,  
  injectTranslations,  
  articlesStore,  
  categoriesStore,  
];

В итоге в контейнер будет добавляться заранее подготовленная коллекция (массив) регистраций. В метод set можно передавать массив массивов регистраций любой вложенности.

// Подключение модуля catalogFeature с массивом регистраций в приложение
container.set([configs, renderService, catalogFeature])

Выбор по токену get(token)

async get<Type>(token: TokenInterface<Type>): Promise<Type>

Любое решение можно выбрать из контейнера по токену методом get(token). Если выбор решения осуществляется впервые, то оно будет создано, инициализировано и возвращено. Если осуществляется повторная выборка решения, то возвращается ранее созданное решение (объект). Если по токену не будет найдена регистрация, то возникнет ошибка.

const i18n = await container.get(I18N);
const logs = await container.get(LOG_SERVICE);

Метод get(token) асинхронный - возвращает Promise. Возможна ситуация, когда повторная выборка решения осуществляется при ещё не выполненной первой выборке с тем же токеном. Но контейнер не будет повторно создавать и инициализировать решение, а вернет обещание (promise) от первой выборки. Все получат один и тот же экземпляр решения.

Выбор по карте токенов getMapped(depends)

async getMapped<Deps extends Record<string, TokenInterface>>(depends: Deps): Promise<TypesFromTokens<Deps>>

Выбор множества решений по указанной карте токенов. Решения будут возвращены под теми же ключами, под которыми указаны токены в аргументе depends. Формат аргумента depends такой же, как в регистрациях зависимостей.

const { i18n, logs } = await container.getMapped({ i18n: I18N, logs: LOG_SERVICE })

Метод асинхронный и будет ждать готовности всех запрошенных решений.

Выбор со Suspense getWithSuspense(token) и getMappedWithSuspense(depends)

getWithSuspense<Type>(token: TokenInterface<Type>): Type

Синхронный выбор программного решения по токену. Но в случаи не готовности запрашиваемого решения будет выкидываться исключение с обещанием (Promise) . Promise не возвращается методом! Если решение уже готово, то будет успешно возвращено.

try {
  const i18n = container.get(I18N);
  console.log('Решение уже было подготовлено!', i18n)
} catch (e) {
  if (e instanceof Promise) {
    console.log('Решение ещё не готово!'))
    e.then((i18n) => console.log('Решение подготовилось!', i18n))
  } else {
    throw e;
  }
}

Метод предназначен для реализации React хуков с обработкой ожидания через компонент <Suspense>, и напрямую редко используется. Аналогичным образом работает метод getMappedWithSuspense(depends) - только выборка уже по карте токенов.

Хуки для выборки из контейнера

Хуки используются в React компонентах для выборки программных решения из DI контейнера. DI контейнер прокидывается в React-контекст сервисом рендера, нл доступ в компонентах предоставляется не к DI контейнеру, а к программным решениям из контейнера. В хуках указываются токены на необходимые решения.

Выбор по токену useSolution(token)

useSolution<Type>(token: Token<Type>): Type

Хук useSolution(token) используется для выборки программного решения из DI контейнера по токену. В React-приложении должен применяться компонент <Suspense>, так как хук может выбросить исключение в случае ожидания готовности выбираемого решения. Это означает, что пока решение не будет готово, выполнение React компонента будет сброшено, и будет отображаться fallback-компонент от <Suspense> (например, спиннер). Как только обещание готовности запрашиваего решения выполнится, компонент будет заново отрендерен, и хук успешно вернёт запрошенное по токену решение.

function MyCompoentn () {
  const modals = useSolution(MODALS);
  const open = () => modals.open(CONFIRM_MODAL)
  return (
    <div onClick={openConfirm}>Отправить</div>
  )
}

Выбор по карте токенов useSolutionMap(depends)

useSolutionMap<Deps extends Record<string, Token>>

Хук useSolutionMap(depends) используется для выборки множества программных решений из DI контейнера по указанной карте токенов. Решения будут возвращены с теми же ключами, что и токены в аргументе depends. Формат аргумента depends аналогичен формату регистраций зависимостей. Логика хука схожа с логикой хука useSolution(token), но обещание готовности будет одно для всех запрашиваемых решений. В случае с раздельным вызовом useSolution компонент будет перерендериваться несколько раз — для каждого решения, которое ещё не готово. С хуком useSolutionMap(depends) выброс обещания в исключение произойдёт один раз — сразу для всех запрашиваемых решений.

const { i18n, store } = useSolutionMap({i18n: I18N_TOKEN, store: STORE_TOKEN})

Выбор со статусами ожидания useSolutionPending(token)

useSolutionPending<Type>(token: Token<Type>): {  
  instance: Type | undefined;  
  isSuccess: boolean;  
  isWaiting: boolean;  
  isError: boolean;  
}

Хук useSolutionPending(token) используется для выборки программного решения из DI контейнера по токену с возвратом признаков ожидания, успеха или ошибки. Для использования хука не требуется компонент <Suspense>, так как хук не выбрасывает исключений.

Если запрашиваемое решение ещё не готово, выполнение компонента не прекратится. Но в этом случае необходимо проверять статусы, которые вернёт хук — isSuccess, isWaiting, isError.

Если решение ещё не готово (так как оно асинхронно создаётся), то оно не будет возвращено, но будет возвращён признак isWaiting = true. Когда решение будет готово, компонент перерендерится, и при следующем вызове хук useSolutionPending вернёт признак isSuccess = true и экземпляр самого решения в instance.

Если при подготовке решения возникнет фатальная ошибка, хук вернёт признак isError = true.

const i18n = useSolutionPending(I18N_TOKEN);

console.log(i18n.instance);
console.log(i18n.isSuccess); 
сonsole.log(i18n.isWaiting); 
console.log(i18n.isError);

if (i18n.instance) {  
  // Действия если сервис i18n получен  
}  

Токен на контейнер CONTAINER

Иногда нужна зависимость на сам контейнер, для этого существует токен на контейнер. Фактически контейнер сам себя регистрирует в контейнере.

const container = useSolution(CONTAINER);

Сброс решения deleteValue(token)

async deleteValue<Type>(token: TokenInterface<Type>): Promise<void>

Удаление экземпляра программного решения в DI контейнере. После чего при выборке этого же решения из DI контейнера оно снова будет создаваться и инициироваться.

События контейнера events

Контейнер отправляет события о создании экземпляра решения onCreate и его удалении onDelete (когда вызывается deleteValue()). На эти события можно подписаться.

container.events.on('onCreate', <Type extends object>({ token, value }: { token: Token<Type>; value: Type }) => {
  // обработка события
}
container.events.on('onDelete', ({ token }: { token: Token }) => {
  // обработка события
}

Например, сервис dump отслеживает событие onCreate, чтобы всем инициализируемым программным решениям сразу передавать их сохраненное состояние (состояние сохраняется после рендере на сервере).

Для работы с событиями используется класс Events из React-Solution. Для отписки от события применяется метод events.off(). Подробнее см. в описании класса Events.

Регистрация | .. →