Экземпляры всех модулей можно сразу создать и поместить в один объект, обеспечив доступ к нужному экземпляру по ключу. Такой объект можно называть контейнером экземпляров. Или локатором, менеджером, реестром. А сами экземпляры можно именовать сервисами, ресурсами, модулями, программными решениями.
const container = {
http: createHttpClient({ baseUrl: '/api/v1' }),
logger: createLogger(),
};
Идея создания контейнера экземпляров продиктована стремлением упростить передачу зависимостей вложенным функциям. Вместо передачи множества отдельных экземпляров можно передать только контейнер экземпляров. Если некой вложенной функции понадобится дополнительный экземпляр, его не придётся указывать в качестве зависимости во всех вышестоящих функциях для проброса. Ведь любой нужный экземпляр можно будет выбрать из контейнера.
Обычно контейнер экземпляров наделяют дополнительной возможностью: он не только предоставляет доступ к общим экземплярам, но и сам создаёт их при первом обращении, а затем запоминает, чтобы избежать повторного создания при последующих обращениях. Получается реализация паттерна проектирования Фабрика с кэшированием.
const createContainer = () => {
let http = null;
let logger = null;
return {
getHttp: () => {
if (!http) http = createHttpClient({ baseUrl: '/api/v1' });
return http;
},
getLogger: () => {
if (!logger) logger = createLogger();
return logger;
},
// ...экземпляры других модулей
};
};
Для получения экземпляра вызывается соответствующий метод контейнера, но без передачи каких-либо аргументов. Если экземпляр ранее уже был создан, то сразу будет возвращен. Для каждого типа экземпляра реализуется свой метод, поэтому в нём можно учесть все особенности создания и инициализации экземпляра.
Использование контейнера экземпляров со всеми описанными свойствами позволяет перенести лишнюю обязанность с главной функции приложения (корневого файла) в отдельный слой подготовки общих экземпляров (в контейнер экземпляров) и отложить их создание до момента реального использования. Можно даже реализовать динамическую загрузку файлов модулей, чьи экземпляры будут создаваться только при необходимости.
Контейнер экземпляров можно сделать единственной зависимостью для всех модулей. Тогда в конструктор каждого модуля будет передаваться ссылка на контейнер для унификации передачи любых зависимостей. При этом сохраняется принцип инверсии зависимостей, так как конкретная реализация определяется самим контейнером. Для изолированного выполнения или тестирования можно подготовить другой контейнер с другими экземплярами. Однако этот момент настораживает...
const createContainer = () => {
let http = null;
let logger = null;
let someService = null;
const container = {
getHttp: () => {
if (!http) http = createHttpClient({ manager, baseUrl: '/api/v1' });
return http;
},
getLogger: () => {
if (!logger) logger = createLogger({ manager });
return logger;
},
getSomeService: () => {
if (!someService) someService = createSomeService({ manager });
return someService;
}
// ...экземпляры других модулей
};
return container;
};
Использование контейнера в качестве единственной зависимости — это шаг в сторону от полноценной инверсии зависимостей. Ведь модули будут сами решать, что выбрать из контейнера. Это приведёт к потере наглядности: станет неочевидным, от чего реально зависит модуль и что должен предоставлять контейнер для корректной работы. Потребуется внимательно исследовать исходный код модуля, выискивая обращения к контейнеру.
const createSomeService = (conianer: Container) => {
const exampleMethod = async () => {
const http = conianer.getHttp(); // Неочевидно сразу, что модуль зависит от http клиента
const response = await http.request('/data');
const logger = conianer.getLogger(); // Неочевидно сразу, что моудлю нужен логгер
//../продолжение логики
};
return { exampleMethod };
};
Кроме того, проблема проброса зависимостей никуда не исчезает. Да, теперь вместо множества зависимостей нужно пробрасывать только контейнер экземпляров. Но если модуль не нуждается в контейнере, его всё равно придётся передавать, чтобы обеспечить доступ к другим моделям через контейнер во вложенных модулях. Например, сервису логирования вряд ли нужен доступ к контейнеру экземпляров, но на всякий случай мы передаём его, чтобы гарантировать доступность на любой глубине в стеке вызова модулей.
Использование контейнера экземпляров добавляет гибкость для расширения функциональности проекта, но не решает проблему проброса зависимостей, а также нарушает инверсию зависимостей. Всё таки необходимо явно передавать или указывать зависимости и найти способ полностью избавиться от их проброса. Эту проблему попробуем решить в следующих темах.