Регистрация зависимостей

В DI контейнере можно зарегистрировать зависимости функции, класса или сразу зарегистрировать значение, чтобы его использовать как зависимость. Удобнее заранее определить параметры регистрации, чтобы не прописывать их каждый раз при добавлении в контейнер. Для этого предусмотрены вспомогательные функции, которые помогают с типизацией.

Регистрация функции injectFactory()

Регистрация функции является универсальной, так как позволяет реализовать любую логику создания и инициализации объекта (программного решения) или просто вернуть уже готовый. Кроме того, функция может быть асинхронной. Через функцию легко интегрировать сторонние решения, например, react-router или redux.

function injectFactory<Type, Deps>(inject: {
  token: Token<Type>
  depends: Deps
  factory: (depends: TypesFromTokens<Deps>) => Type | Promise<Type>
}): InjectFactory<Type, Deps>

Функция указывается в свойстве factory (от англ. "фабрика"). Необходимые зависимости перечисляются в свойстве depends в виде объекта, где каждый ключ должен содержать токен, соответствующий типу зависимости. Все зависимости будут переданы в первый аргумент функции factory: (depends) => {} в виде объекта, свойства которого будут иметь те же названия, что и в описании инъекции. Функция factory должна вернуть объект, тип которого сопоставим с токеном инъекции.

Благодаря применению дженериков TypeScript, тип результата функции будет автоматически сопоставлен с типом токена, а тип аргумента функции factory будет сопоставлен с токенами в depends.

Пример регистрации вымышленного стороннего решения SomeLibrary с особым способом инициализации.

const SOME = newToken<SomeLibrary>('my-project/some');

const someInjection = injectFactory({
  token: SOME,
  depends: { logger: LOGS },
  factory: async ({ logger }) => {
    // Пример стороннего решения со специфической логикой инициализации
    const some = new SomeLibrary().setLogger(logger);
	await some.setSomeOption('option');		
	return some;
  }
})

При создании регистрации не нужно беспокоиться о том, откуда возьмутся указанные зависимости (реальные объекты, сервисы...). В регистрации лишь указывается, что необходимо для функции factory, что она должна вернуть, и сама функция factory. Созданную регистрацию остаётся лишь добавить в DI контейнер в главном файле:

function createSolutions(envPatch: Patch<Env> = {}): Container {
    return new Container() 
      //....
      .set(someInjection) // <--- добавление регистрации
}

Экземпляр стороннего решения SomeLibrary теперь доступен из любого места в программном коде: его можно выбрать вручную из контейнера по токену SOME или автоматически как зависимость.

Регистрация класса injectClass()

Для регистрации класса нужно учесть одно условие — все зависимости передаются в первый аргумент конструктора в виде mapped-объекта, так же, как это происходит с функцией factory. При самостоятельной реализации классов это условие легко соблюдается, и в регистрацию можно передать сам класс (по сути, передаётся функция-конструктор).

function injectClass<Type, Deps>(inject: {
  token: Token<Type>
  depends: Deps;
  constructor: new (depends: TypesFromTokens<Deps>) => Type
}): InjectClass<Type, Deps>

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

class First {
  contructor(protected depends: {
    logger: ILogger,
    config: Patch<FirstConfig>
  }) {
    //...
  }
  
  exmaple() {
    this.depends.logger.error('Метод ещё в разработке');
  }
}

Создайте токен для типа First и токен для настроек Patch<FirstConfig>. Тип настроек оборачивается в Patch<>, чтобы можно было переопределять только необходимые параметры, а не передавать все целиком.

export FIRST = newToken<First>('my-project/first')
export FIRST_CONFIG = newToken<Patch<FirstConfig>>('my-project/first/config')

Подготовить регистрацию с перечнем всех зависимостей и конструктором класса:

export firstInjection = injectClass({
  token: FIRST,
  depends: { logger: LOGS, config: FIRST_CONFIG },
  constructor: First
})

И добавить регистрацию в DI контейнер в главном файле:

function createSolutions(envPatch: Patch<Env> = {}): Container {
    return new Container() 
      //....
      .set(firstInjection)
}

Экземпляр класса First теперь доступен из любого места в программном коде: его можно выбрать вручную из контейнера по токену FIRST или автоматически как зависимость.

Регистрация значения injectValue()

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

function injectValue<Type>(inject: { token: Token<Type>, value: Type }): InjectValue<Type>

Регистрировать в виде значения можно всё, что можно создать заранее, что не будет изменяться в процессе выполнения программы и не нуждается в зависимостях. Например, это могут быть переменные окружения, настройки сервисов, словари для локализации, данные-заглушки, глобальные константы и прочее. В примере ниже показана регистрация двух переменных окружения.

const envInjection = injectValue({  
  token: ENV,  
  value: {  
    MODE: process.env.NODE_ENV || 'development',  
    PROD: !process.env.NODE_ENV || process.env.NODE_ENV === 'production', 
  },  
})

Регистрация изменений

Регистрация изменений позволяет расширить уже внедрённую регистрацию. По умолчанию, если в контейнер внедрить регистрацию с токеном, по которому уже была сделана регистрация, то новая регистрация просто заменит старую — никакого расширения не произойдёт. Однако, если в регистрации указать свойство merge: true, то контейнер учтёт новую регистрацию, не игнорируя старую. Все регистрации будут выполнены, и их результаты будут объединены в одно общее значение с помощью алгоритма глубокого слияния объектов. Объединяются не сами регистрации, а их результаты! Конечно, слияние не подходит для всех типов данных. Например, не стоит использовать регистрацию изменений для расширения экземпляров сервисов. Регистрация изменений идеально подходит для расширения объектов, тип которых предусматривает слияние, например объект с переменными окружения и другие подобные данные.

const envInjection = injectValue({
  token: ENV,  
  value: {  
    NEW_VALUE: 'new environment' 
  },
  merge: true  // <-- выполнять слияние с существующей регистрацией
})

Регистрацию изменений можно использовать для расширения сервисов через расширение их настроек или используемых ресурсов. Например, у сервиса мультиязычности есть зависимость от словаря локализации, и этот словарь можно дополнять через регистрацию изменений! Каждый модуль приложения может определить свой собственный словарь и внедрить его в контейнер под токеном I18N_DICTIONARY.

export const injectTranslations = injectValue({  
  token: I18N_DICTIONARY,  
  value: {  
    'en-EN': { // <-- Английский словарь дополнится переводами каталога
      catalog: { // <-- Если есть уже каталог, то просто дополнится словами title и reset
        title: "List & filter",  
        reset: "Reset" 
      }
    },  
    'ru-RU': { 
      catalog: {
        title: "Список и фильтр",  
         reset: "Сброс"
       }
     },  
  },  
  merge: true, // <-- объединять с существующими регистрацией
});

В контейнер можно добавить любое количество регистраций с одинаковым токеном. Результат выполнения регистрации с свойством merge: true будет объединяться с результатом предыдущей регистрации. А регистрация без merge (или с merge: false) будет заменять все предыдущие регистрации с тем же токеном.

Токен | Контейнер