С помощью React-Solution в главном файле src/index.tsx
создаётся контейнер для управления зависимостями (Dependency Injection Container, DI контейнер). В контейнер добавляются инструкции по созданию всех программных решений с учётом их зависимостей: сервисы, модули, сущности, ресурсы и т. д. Этот этап называется регистрацией зависимостей.
function createSolutions(envPatch: Patch<Env> = {}): Container {
return new Container()
.set(envClient(envPatch))
.set(configs)
.set(renderService) // <-- Регистрация сервиса рендера на React
.set(routerService)
.set(modalsService)
.set(httpClient)
.set(i18nService)
.set(logService)
.set(dumpService)
.set({
token: RENDER_COMPONENT,
depends: { router: ROUTER_SERVICE },
factory: ({ router }) => (
<RouterProvider router={router}>
<App />
</RouterProvider>
),
});
}
Ключевой регистрацией является регистрация зависимостей для сервиса рендеринга на React — renderService
. Параметры регистрации уже подготовлены в исходниках фреймворка, чтобы упростить процесс добавление в DI контейнер. То же самое касается других регистраций — routerService
, modalsService
и т.д.
Для отдельного объявления параметров регистрации используются вспомогательные функции injectClass()
, injectFactory()
или injectValue()
. Эти функции нужны только для типизации регистрации.
const renderService = injectClass({
token: RENDER_SERVICE, // <-- Токен - идентфиикация решения соотв. типа (класса)
constructor: RenderService, // <-- Класс (конструктор) программного решения
depends: { // <-- Зависимости, которые нужны классу RenderService
env: ENV, // <-- Указывается токен, чтобы по нему контейнер нашел программное решение
container: CONTAINER,
logger: LOG_SERVICE,
config: optionalToken(RENDER_CFG), // Необязательная зависимость
dump: optionalToken(DUMP_SERVICE),
children: optionalToken(RENDER_COMPONENT),
},
});
Регистрация содержит токен для идентификации решения (token
), перечень зависимостей (depends
) и функцию создания объекта (factory
или constructor
). В функцию factory
или в constructor
класса будут передаваться программные решения, чьи токены указаны в depends
. Порядок зависимостей в depends
может быть любым, главное — указать все необходимые зависимости для программного решения, в данном случае — для конструктора класса RenderService
.
Порядок регистраций в DI контейнер также не имеет значения, главное, чтобы все необходимые регистрации были добавлены, чтобы все программные решения смогли получить свои зависимости.
Программное решение RenderService
, называемое также сервисом, представляет собой простой класс, не обременённый архитектурными особенностями DI. Самостоятельно класс не импортирует и не выбирает зависимости — все зависимости ожидаются в первом аргументе конструктора. Первый аргумент конструктора отмечен ключевым словом protected
, что позволяет автоматически превращать его в поле класса, хотя это и не обязательно. Ко всем зависимостям в классе можно будет обратиться через поле this.depends
. Название первому аргументу, как и названия зависимостям, можно дать любые.
class RenderService {
constructor(protected depends: {
env: Env;
container: Container;
logger: LogInterface;
config?: Patch<RenderConfig>;
dump?: DumpService;
children?: ReactNode;
}) {
//...
}
example() {
this.depends.logger.info('Обратились к зависимости logger');
}
}
У RenderService
есть множество зависимостей, и для корректной работы все зависимости должны быть внедрены в контейнер. Некоторые зависимости являются опциональными. Например, можно не внедрять настройки, в этом случае будут использованы настройки по умолчанию. Опциональность зависимости учитывается в классе RenderService
, а через опциональный токен optionalToken(RENDER_CFG)
контейнеру сообщается, что зависимость является необязательной.
DI контейнер наполняется всеми регистрациями в главном файле src/index.tsx
для разрешения всех зависимостей. Однако регистрация не обрабатывается сразу при добавлении в контейнер, функция или конструктор в ней не вызывается сразу. Исполнение происходит только при запросе программного решения из контейнера с указанием соответствующего токена методом get(token)
. Контейнер найдёт регистрацию по токену и, если она ещё не была выполнена, подготовит все зависимости, выбрав их с помощью того же метода get
, и выполнит функцию (или конструктор), указанную в регистрации, передав в неё зависимости. Передача зависимостей называется внедрением зависимостей или инъекцией зависимостей.
Если функция из регистрации уже была исполнена, то будет возвращён ранее полученный от неё результат. Таким образом, объекты, выбираемые из контейнера, будут создаваться в единственном экземпляре при первом обращении, а при последующих обращениях будут возвращаться ранее созданные экземпляры.
В том же файле src/index.tsx
, после того как DI контейнер будет создан и наполнен регистрациями зависимостей, необходимо запустить основную логику приложения.
Из контейнера выбирается главное программное решение приложения — сервис рендеринга, и запускается его работа. Сервис рендеринга оказывается инициатором выполнения всего приложения, поэтому его явно выбирают из контейнера и запускают. В свою очередь, уже контейнер создаёт другие объекты (сервис логирования, сервис дампа, React-элемент и другие объекты), чтобы передать их в конструктор сервиса рендеринга. В процессе выполнения программы из контейнера будут извлекаться и другие объекты — где-то явно, а где-то автоматически для внедрения в качестве зависимостей.
Для выборки объекта (программного решения) из контейнера нужно указать только токен. Не нужно заботиться о дополнительных параметрах или инициализациях объекта — просто берём его из контейнера и используем.
const render = await solutions.get(RENDER_SERVICE); // выбор сервиса из контейрера solutions
render.start();
Сервис рендеринга выбирается по токену RENDER_SERVICE
, после чего вызывается метод start()
для запуска рендеринга в браузере. В метод start()
не передаётся никаких параметров, хотя можно было бы ожидать передачу React-элемента для рендеринга. Дело в том, что корневой React-элемент часто оборачивается провайдерами для установки в контекст роутера, хранилища и прочего, что является зависимостями для React-элемента. А для управления зависимостями имеется DI контейнер. Поэтому React-элемент и его зависимости регистрируются в DI контейнере. React-элемент автоматически получит все свои зависимости перед рендером.
.set({
token: RENDER_COMPONENT,
depends: { router: ROUTER_SERVICE },
factory: ({ router }) => (
<RouterProvider router={router}>
<App />
</RouterProvider>
),
})
Все, что нужно React-элементу, указывается в depends
и будет передано в функцию factory
, которая вернёт подготовленный React-элемент. Сервис рендеринга получит этот элемент для рендеринга по токену RENDER_COMPONENT
, так как у сервиса рендеринга есть зависимость children
с этим токеном. Все остальные React-элементы и компоненты не нужно регистрировать в DI контейнере, если только не требуется слабая связанность между модулями и UI приложения.
Внедрение зависимостей (DI) необходимо для слабой связанности между различными частями приложения и для легкой адаптации приложения под разные условия. Внедрение зависимостей обеспечивает инверсию управления, когда программный объект самостоятельно не импортирует и не выбирает себе зависимые объекты, а получает их от инициатора действия. Это касается даже внешних библиотек - вместо того чтобы их импортировать, возможно, правильней будет получать их от DI, абстрагируясь от конкретной реализации.
За счёт слабой связанности программные объекты (решения) можно разрабатывать независимо друг от друга в разных репозиториях разными командами и легко переключаться на разные реализации, адаптируя программу под разные условия. Замена инъекции в контейнере приводит к замене используемого объекта во всём приложении. И не нужно будет по всему исходному коду менять логику передачи зависимостей. Например, сервис кэширования в оперативной памяти легко подменить на сопоставимый сервис кэширования в файлы. Или применять разные реализации сервисов на клиенте и на бэкенде. Возможность подмены через DI активно применяется для функционального тестирования с использованием моковых версий сервисов, например, чтобы не выполнять реальные запросы к АПИ в автоматических тестах.