На работе в проекте столкнулись с достаточно распространенной архитектурной проблемой. Есть набор сервисов, зависящих друг от друга. Первоначально взаимосвязи между сервисами описывались c помощью DI через конструктор.
interface Options {
service1: Service1;
service2: Service2;
serviceN: ServiceN;
}
class ServiceNew {
constructor(private options: Options) {}
}
В большинстве случаев такой подход является достаточно эффективным. Однако порой возникают ситуации, например, когда 2 сервиса зависят друг от друга и приходится инжектить зависимость через функции-геттеры.
class Sevice1 {
constructor(
private getService2(): Service2,
private serviceN: ServiceN
) {}
}
class Sevice2 {
constructor(
private getService1(): Service1,
private serviceN: ServiceN
) {}
}
В ситуации когда таких сервисов например 20, то хочется уже более единообразного подхода. Например мы можем обратиться к опыту языка Java и сохранить все сервисы в один объект и передать ссылку на него в каждый сервис. А для большей изоляции кода можно на уровне типов ограничить список используемых сервисов. Главное условие при таком подходе, что нельзя обращаться к такому объекту в конструкторе, тк в этот момент еще идет наполнение этого объекта. При этом остается декларативность в описании зависимостей и сохраняется единообразность подхода. Также если в каком-то сервисе мы вдруг забудем про объявлении поля ctx
, то type-checker об этом напомнит.
interface IServiceCtx {
service1: Service1;
service2: Service2;
// .....
serviceN: ServiceN;
}
class Service1 {
ctx!: Pick<IServiceCtx, 'service2' | 'serviceN'>
}
class Service2 {
ctx!: Pick<IServiceCtx, 'service1' | 'serviceN'>
}
const ctx: IServiceCtx = {
service1: new Service1(),
service2: new Service2(),
// ....
serviceN: new ServiceN(),
}
Object.values(ctx).forEach(instance => {
instance.ctx = ctx;
});
Для большей унификации можно воспользоваться следующим сниппетом кода.
export function injector<T>(
init: { [K in keyof T]: () => T[K]; },
inject: (instance: T[keyof T], val: T) => void,
): T {
const result = {} as Record<string, unknown>;
Object.entries(init).forEach(([key, fn]) => {
result[key] = (fn as () => unknown)();
});
Object.values(result).forEach(ref => {
inject(ref as T[keyof T], result as T);
});
return result as T;
}
const ctx = injector<IServiceCtx>({
service1: new Service1(),
service2: new Service2(),
// ....
serviceN: new ServiceN(),
}, (instance, ctx) => {
instance.inject = ctx;
// здесь можно сделать какие-то дополнительные действия
});