#DI как в Effect Pt1
Мааам можно мне: "Effect-like-DI"
Нет! У нас "Effect-like-DI" есть дома
Дома: Reader
После прочтения заметки вы будете понимать шутку.
Часто вижу, что хвалят DI в Effect.
Хочу поделиться дедовским рецептиком наколенной версии.
Во первых каждое вычисление должно трекать необходимую зависимость на уровне типов — в Effect это третий типопараметр R.
interface Computation<out Value, in Services extends {} = {}> {
(services: Services) => Value
}
и так давайте вычислением будет функция которая чтобы посчитать
Value ожидает на вход некие сервисы
Services — их будем передавать прям мапой
{ serviceName: serviceImplementation }Давайте сразу аналог
Effect.sync сделаем — вычисление которое не требует никаких
Services —
Computation<Value>
const sync = <Value,>(cb: () => Value): Computation<Value> => () => cb()
А теперь запускатор этих самых вычислений:
const run = <A, R extends {}>(reader: {} extends R ? Reader<A, R> : never) =>
reader({} as R);
{} extends R — это условие как раз гарантирует что для корректного запуска функции ненужны никакие зависимости, собственно можно запустить функции просто вызовом, который потребует передать необходимые аргументы-сервисы или
{} если ничего не требуется
Чтож бахнем хеллоу-ворлд:
const helloDI: Computation<void> = sync(() => console.log("Hello, DI!"))
const result = run(helloDI) // => void
А теперь давайте-ка вынесем наш логгер в сервис и доработаем функцию
interface Logger {
log: (...args: ReadonlyArray<unknown>) => void
}
const ConsoleLogger: Logger = globalThis.console
const helloDI: Computation<void, { logger: Logger }> = (services) => services.logger.log("Hello, DI!")
run(helloDI) // Error
Получаем ошибку — функция требует чтобы мы предоставили сервис-логгер
Давайте напишем функцию которая провайдит зависимость! Нужно держать в уме, что
Services это по сути Key-Value мапка, а пробрасывать зависимости будем прям подмножеством этой мапки
Partial<Services>, при этом из первоначальной мапки будем омитить то что пробросили
Omit<Services, keyof Provided>
const provideServices =
<Value, Services extends {}>(computation: Reader<Value, Services>) =>
<Provided extends Partial<Services>>(provided: Provided): Reader<Value, Omit<Services, keyof Provided>> =>
(services) => {
const value = computation({ ...req, ...provided } as {} as Services);
return value;
};
Погнали пробросим зависимость:
const runnable = provideServices(helloDI)({ logger: ConsoleLogger })
run(runnable) // OK
А теперь давайте также будем выводить в консоль рандомные числа!
interface Random {
random: () => number;
}
const MathRandom: RandomService = globalThis.Math
const getRandom: Computation<void, { random: Random }> = (services) => services.random()
const logNumber = (num: number): Computation<void, { logger: Logger }> = (services) => services.logger.log(`Random: ${num}`)
Теперь нужно связать эти две функции, чтобы вернулась новая. Понятно чтоб мы сначала получим рандомное чисто и потом его залогируем, но для этого нам понадобятся оба сервиса — рандом & логгер
const andThen =
<Serivces1 extends {}, Value1>(computation1: Computation<Value1, Serivces1>) =>
<Value2, Serivces2 extends {}>(
fn: (a: Value1) => Computation<Value2, Services2>,
): Computation<Value2, Services1 & Services2> => {
return (r: Services1 & Services2) => {
const value1 = computation(r); // тут из общих сервисов возьмем только Services1
const computation2 = fn(value1);
const value2 = computation2(r) // тут из общих сервисов возьмем только Services2
return value2;
};
};