Комментарии 8
Спасибо большое за комментарий! Вы не первый, кто написал о том, что не хватает реальных практических примеров для демонстрации подхода. Я учту это в своих будущих работах.
Все-таки
multi
существует для того, когда есть задача получить по ключу именно массив, а не одну сущность.
Согласен. Стоило упомянуть привычные кейсы использования multi
, такие как интерсепторы или VALUE_ACCESSOR
, для более подробного описания темы. Однако в данной статье речь больше про кейсы, когда требуется регистрировать список различных реализаций и уметь получать необходимую реализацию, в зависимости от внешних факторов.
С этим я согласен, вот только
multi
тут ни к чему. Мы можем переопределять реализацию зависимости на разных уровнях иерархии компонентов...
Речь идёт о регистрации реализаций в один скоуп в иерархии для дальнейшего взаимодействия с ними, как вы написали чуть ниже. Давайте приведу несколько примеров, где это может оказаться удобным.
Пример 1

Представим, что мы разрабатываем конструктор форм (для примера взят скриншот с яндекс форм). В данном случае мы создам некоторый контракт IWidgetConfigurator
, который будет иметь поле key
и прочие общие методы для работы с виджетом. Для каждого виджета будет создана своя реализация данного интерфейса, которая будет предоставлять специфические данные, настройки, модели для конкретного виджета (можно придумать разные отвественности для этого сервиса). Во время драг-н-дропа виджета из сайдбара на форму у нас будет появляться компонента данного виджета (в примере "Один вариант"). Все эти виджеты будут унаследованы от базовой компоненты виджета и иметь некоторое абстрактное поле type
, которое будет реализовано в наследниках. По этому полю type
мы сможем получать из сервиса-менеджера необходимую реализацию для настройки активного виджета.
Пример 2
За вторым примером далеко ходить не пришлось — конструтор параграфа на хабре тоже мог бы быть реализован с помощью данного подхода.

В данном случае у нас есть компонента конструктора параграфа, в которую мы можем зарегистрировать список виджетов. Каждый виджет будет реализовывать некоторый интерфейс.
interface ITextWidget implements IKeyed<WidgetType> {
key: WidgetType;
/** Добавляет виджет в параграф */
addWidgetTo(paragparh: ParagraphModel): void;
}
Также есть селект-контрол, который содержит список опций в форматe { имя_виджета, ключ_виджета }
. Когда мы кликаем на опцию селекта происхдит valueChanges
с новым ключом и мы можем добавить виджет на параграф, выбрав реализацию из менеджера по ключу виджета.
const provideTextWidgets = provideMultiService.bind(null, {
managerType: TextWidgetsManager,
serviceToken: TEXT_WIDGET_TOKEN,
registerEach: false,
services: [
new QuoteTextWidget(),
new OrderedListTextWidget(),
new ImageTextWidget(),
new CodeTextWidgetd(),
// ...,
new MyCoolTextWidget(),
],
});
@Component({
providers: [
...provideTextWidgets()
],
})
class ParagraphConstructorComponent implements OnInit {
private readonly select: FormControl = new FormControl<string | null>(null);
private readonly paragraphModel: ParagraphModel = new ParagraphModel();
private readonly _widgetsManager: TextWidgetsManager = inject(TextWidgetsManager);
public ngOnInit(): void {
this.select.valueChanges
.pipe(
takeUntil()
)
.subscribe((key: WidgetType) => {
this._widgetsManager.get(key).addWidgetTo(this.paragraphModel);
});
}
}
В данных случаях использоавние описанного подхода считаю достаточно уместным, оправданным и удобным.
А разве не то же самое реализует механизм DI, используемый ангуляром?
provider: {provider: providerToken ... }
В чем практический смысл? И как понимать: какие сервисы зарегистрированны, а какие - нет?
А разве не то же самое реализует механизм DI, используемый ангуляром?
В ангуляре реализуется паттерн Injection Key, который позволяет регистрировать зависимости в DI-контейнер по уникальному ключу, а затем получать их с помощью этого ключа. Однако, возникают ситуации, что по одному ключу регистрируется список зависимостей с помощью опции провайдера multi: true
. Такой подход подразумевает, что мне нужно как-то получать конкретную реализацию из списка. То есть связь между ключём и значением становится не one to one, а one to many.
В чем практический смысл?
Представим, что у меня есть 10 сервисов, которые я регистрирую, через multi.
[
{
provide: EXAMPLE_SERVICE_TOKEN,
useClass: ExmapleService1,
multi: true,
},
{
provide: EXAMPLE_SERVICE_TOKEN,
useClass: ExmapleService2,
multi: true,
},
// ...
{
provide: EXAMPLE_SERVICE_TOKEN,
useClass: ExmapleService10,
multi: true,
}
]
Если я попытаюсь сделать inject(EXAMPLE_SERVICE_TOKEN)
, то вместо одного инстанса получу список инстансов. Но как мне выбрать нужный инстанс из списка? Допустим у меня есть некоторый select-control, и я хочу, чтобы из DI бралась новая реализация, каждый раз, когда отрабатывает valueChanges
, в зависимости от значения селекта.
Как раз для этого мне будет удобно использовать сервис-менеджер, чтобы по ключу получать конкретный сервис из списка.
И как понимать: какие сервисы зарегистрированны, а какие - нет?
Например можно создать в сервисе-менеджере метод has(key: TKey): boolean
, который будет говорить зарегистрирован ли сервис по такому ключу или нет. Для этой задачи можно придумать множество решений и расширить базовый функционал.
А зачем регистрировать под multi то, что должно быть потом получено в единственном значении? В чем проблема выделить под них отдельное имя?
Под один токен регистрируются разные реализации одного контракта, которые могут подменяться в зависимости от состояния приложения. Если бы у вас было три реализации и каждая была бы зарегистрированна отдельно, вто в коде вам бы приходилось получать все три релизации, а затем определять которую из них использовать.
@Component({
providers: [
ExampleServiceA,
ExampleServiceB,
ExampleServiceC
]
})
export class App {
private readonly _exampleServiceA: ExampleServiceA = inject(
ExampleServiceA
);
private readonly _exampleServiceB: ExampleServiceA = inject(
ExampleServiceB
);
private readonly _exampleServiceC: ExampleServiceA = inject(
ExampleServiceC
);
public log(key: ExampleServiceKey): void {
if (key === ExampleServiceKey.A) {
alert(this._exampleServiceA.getData());
} else if (key === ExampleServiceKey.B) {
alert(this._exampleServiceB.getData());
} else
alert(this._exampleServiceC.getData());
}
}
Спасибо за статью, изложено структурированно! Но все же в вашей аргументации нахожу нестыковки.
Однако, возникают ситуации, что по одному ключу регистрируется список зависимостей с помощью опции провайдера
multi: true
. Такой подход подразумевает, что мне нужно как-то получать конкретную реализацию из списка.
Все-таки multi
существует для того, когда есть задача получить по ключу именно массив, а не одну сущность. В доке Ангуляра это тоже обговорено, правда, косвенно, через пример. В итоге multi: true
и "получить конкретную реализацию из списка" это противоположные по смыслу вещи.
Под один токен регистрируются разные реализации одного контракта, которые могут подменяться в зависимости от состояния приложения
С этим я согласен, вот только multi
тут ни к чему. Мы можем переопределять реализацию зависимости на разных уровнях иерархии компонентов, но выбрать, какую конкретно реализацию внедрить потребителю – задача DI-контейнера, а не пользовательского кода внутри компонента. Иначе вы частично на себя берете логику DI, что выглядит избыточно. Также при вашем подходе все реализации должны быть предоставлены на одном уровне в одном месте через provideMultiService
, что в реальной жизни довольно редко встречается.
Если по какой-то причине вам в одном компоннете в разные моменты времени нужны сразу три разных реализации сервиса, как в примере выше, то почему бы просто не внедрить инжектор? В качестве ключа использовать не строки, а собственно токены – будь то типы сервисов или инстансы InjectionToken
.
type ExampleServiceToken =
| typeof ExampleServiceA
| typeof ExampleServiceB
| typeof ExampleServiceC;
@Component({
providers: [
ExampleServiceA,
ExampleServiceB,
ExampleServiceC
],
})
export class App {
private readonly injector = inject(Injector);
public log(token: ExampleServiceToken): void {
const service = this.injector.get(token);
alert(service.getData());
}
}
При такой реализации становится неясно, зачем нужны предложенные в статье обертки в виде менеджеров и т.д. Получать по ключу сущность – задача инжектора и он отлично с этим справляется.
Возможно, вы могли бы привести примеры задач из реальной жизни, где просто инжектора недостаточно и решениеч через менеджер действительно облегчило вам жизнь? Пока что трудно такое представить.
Спасибо большое за комментарий! Вы не первый, кто написал о том, что не хватает реальных практических примеров для демонстрации подхода. Я учту это в своих будущих работах.
Все-таки
multi
существует для того, когда есть задача получить по ключу именно массив, а не одну сущность.
Согласен. Стоило упомянуть привычные кейсы использования multi
, такие как интерсепторы или VALUE_ACCESSOR
, для более подробного описания темы. Однако в данной статье речь больше про кейсы, когда требуется регистрировать список различных реализаций и уметь получать необходимую реализацию, в зависимости от внешних факторов.
С этим я согласен, вот только
multi
тут ни к чему. Мы можем переопределять реализацию зависимости на разных уровнях иерархии компонентов...
Речь идёт о регистрации реализаций в один скоуп в иерархии для дальнейшего взаимодействия с ними, как вы написали чуть ниже. Давайте приведу несколько примеров, где это может оказаться удобным.
Пример 1

Представим, что мы разрабатываем конструктор форм (для примера взят скриншот с яндекс форм). В данном случае мы создам некоторый контракт IWidgetConfigurator
, который будет иметь поле key
и прочие общие методы для работы с виджетом. Для каждого виджета будет создана своя реализация данного интерфейса, которая будет предоставлять специфические данные, настройки, модели для конкретного виджета (можно придумать разные отвественности для этого сервиса). Во время драг-н-дропа виджета из сайдбара на форму у нас будет появляться компонента данного виджета (в примере "Один вариант"). Все эти виджеты будут унаследованы от базовой компоненты виджета и иметь некоторое абстрактное поле type
, которое будет реализовано в наследниках. По этому полю type
мы сможем получать из сервиса-менеджера необходимую реализацию для настройки активного виджета.
Пример 2
За вторым примером далеко ходить не пришлось — конструтор параграфа на хабре тоже мог бы быть реализован с помощью данного подхода.

В данном случае у нас есть компонента конструктора параграфа, в которую мы можем зарегистрировать список виджетов. Каждый виджет будет реализовывать некоторый интерфейс.
interface ITextWidget implements IKeyed<WidgetType> {
key: WidgetType;
/** Добавляет виджет в параграф */
addWidgetTo(paragparh: ParagraphModel): void;
}
Также есть селект-контрол, который содержит список опций в форматe { имя_виджета, ключ_виджета }
. Когда мы кликаем на опцию селекта происхдит valueChanges
с новым ключом и мы можем добавить виджет на параграф, выбрав реализацию из менеджера по ключу виджета.
const provideTextWidgets = provideMultiService.bind(null, {
managerType: TextWidgetsManager,
serviceToken: TEXT_WIDGET_TOKEN,
registerEach: false,
services: [
new QuoteTextWidget(),
new OrderedListTextWidget(),
new ImageTextWidget(),
new CodeTextWidgetd(),
// ...,
new MyCoolTextWidget(),
],
});
@Component({
providers: [
...provideTextWidgets()
],
})
class ParagraphConstructorComponent implements OnInit {
private readonly select: FormControl = new FormControl<string | null>(null);
private readonly paragraphModel: ParagraphModel = new ParagraphModel();
private readonly _widgetsManager: TextWidgetsManager = inject(TextWidgetsManager);
public ngOnInit(): void {
this.select.valueChanges
.pipe(
takeUntil()
)
.subscribe((key: WidgetType) => {
this._widgetsManager.get(key).addWidgetTo(this.paragraphModel);
});
}
}
В данных случаях использоавние описанного подхода считаю достаточно уместным, оправданным и удобным.
Отличная статья, спасибо!
Единственное что хотелось бы увидеть сверх сказанного - пару примеров, чтоб в голове, помимо абстракции, была связь с реальным миром. Вдруг кому-то поможет.
Продвинутая регистрация multi-сервисов в Angular