Как стать автором
Обновить

Комментарии 8

ЗакрепленныеЗакреплённые комментарии

Спасибо большое за комментарий! Вы не первый, кто написал о том, что не хватает реальных практических примеров для демонстрации подхода. Я учту это в своих будущих работах.

Все-таки multi существует для того, когда есть задача получить по ключу именно массив, а не одну сущность.

Согласен. Стоило упомянуть привычные кейсы использования multi, такие как интерсепторы или VALUE_ACCESSOR, для более подробного описания темы. Однако в данной статье речь больше про кейсы, когда требуется регистрировать список различных реализаций и уметь получать необходимую реализацию, в зависимости от внешних факторов.

С этим я согласен, вот только multi тут ни к чему. Мы можем переопределять реализацию зависимости на разных уровнях иерархии компонентов...

Речь идёт о регистрации реализаций в один скоуп в иерархии для дальнейшего взаимодействия с ними, как вы написали чуть ниже. Давайте приведу несколько примеров, где это может оказаться удобным.

Пример 1

Рис 1 - Функционал конструктора форм
Рис 1 - Функционал конструктора форм

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

Пример 2

За вторым примером далеко ходить не пришлось — конструтор параграфа на хабре тоже мог бы быть реализован с помощью данного подхода.

Рис 2 - Функционал конструктора параграфа
Рис 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

Рис 1 - Функционал конструктора форм
Рис 1 - Функционал конструктора форм

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

Пример 2

За вторым примером далеко ходить не пришлось — конструтор параграфа на хабре тоже мог бы быть реализован с помощью данного подхода.

Рис 2 - Функционал конструктора параграфа
Рис 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);
      });
  }
}

В данных случаях использоавние описанного подхода считаю достаточно уместным, оправданным и удобным.

Отличная статья, спасибо!

Единственное что хотелось бы увидеть сверх сказанного - пару примеров, чтоб в голове, помимо абстракции, была связь с реальным миром. Вдруг кому-то поможет.

Спасибо за отзыв! Постараюсь добавлять больше конкретных примеров в будущем.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории