Введение
Архитектуру современного приложения практически невозможно представить без инверсии управления. Внедрение зависимостей является реализацией этого подхода и в isomorph оно реализовано исходя из следующих ограничений:
- Не используем рефлексии (
reflect-metadata
), так как эта надстройка много весит и медленно работает - Не используем декораторы, так как они пока не попали в стандарт ECMAScript
Итоговый размер сборки важен, так как isomorph используется в том числе в браузерных приложениях.
DI-контейнер
Для внедрения зависимостей пакет предоставляет реализацию DI-контейнера. Исходя из заявленных ранее ограничений, реализовать автоматическую подстановку зависимостей на основе рефлексий не представляется возможным. Эта проблема решается наличием ряда архитектурных ограничений.
На данный момент все компоненты в рамках DI-контейнера являются "синглтонами".
Термины
Токен - уникальное значение, которое можно использовать для регистрации компонента в DI-контейнере.
Можно было бы использовать уникальные строки или символы, однако токен позволяет закрепить за собой конкретный интерфейс. В разработке это помогает автоматически выводить типы а также предупреждать о том, что реализация не соответствует интерфейсу при регистрации.
Провайдер - функция, которая занимается предоставлением реализации интерфейса.
Функция-провайдер получает в качестве аргумента функцию-геттер зависимостей. Обычно провайдер просто предоставляет зависимости для реализации но может также выбрать реализацию.
Ограничения
- реализации не должны ниче го знать про контейнер/токены, для этого есть провайдеры
- стоит вызывать resolve только в рамках функции-провайдера
- стоит разделять интерфейс и его реализацию по разным файлам
Пример использования
Для начала создадим файл со всеми токенами. Предполагается, что у нас уже есть интерфейсы компонентов нашего приложения.
import { createToken } from '@sima-land/isomorph/di';
import type { Config, Logger, Server } from './interfaces';
export const TOKEN = {
config: createToken<Config>('config'),
logger: createToken<Logger>('logger'),
server: createToken<Server>('server'),
} as const;
Опишем DI-контейнер.
import { createContainer, Resolve } from '@sima-land/isomorph/di';
import { TOKEN } from './tokens';
import express from 'express';
export const container = createContainer();
container.set(TOKEN.config, provideConfig);
container.set(TOKEN.logger, provideLogger);
container.set(TOKEN.server, provideServer);
function provideConfig(): Config {
return {
name: 'example-app',
port: Number(process.env.PORT),
};
}
function provideLogger(): Logger {
return {
info(data) {
console.log(data);
},
};
}
function provideServer(resolve: Resolve): Server {
const logger = resolve(TOKEN.logger);
const app = express();
app.get('/home', (req, res) => {
logger.info(`Incoming request, timestamp: ${Date.now()}`);
res.send('Hello, world!');
});
return app;
}
Теперь мы можем использовать контейнер в точке входа.
import { container } from './container';
import { TOKEN } from './tokens';
const server = container.get(TOKEN.server);
const logger = container.get(TOKEN.logger);
const config = container.get(TOKEN.config);
server.listen(config.port, () => {
logger.info(`Server started on port ${config.port}`);
});