В 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
) будет заменять все предыдущие регистрации с тем же токеном.