Внедрение нескольких реализаций для одного и того же провайдера в Angular

Проблема

В моем шаблоне Angular есть следующие настройки:

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table>
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Я хочу, чтобы в каждый из app-custom-table была внедрена отдельная реализация сервиса, чтобы иметь возможность отображать и обрабатывать разные данные.

Идея

На самом деле я пытаюсь изменить существующее решение, которое включало передачу службы в качестве аргумента @Input() непосредственно каждому из компонентов. Вот так:

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table [service] = "serviceImpl1"
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table [service] = "serviceImpl2">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table [service] = "serviceImpl3">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table [service] = "serviceImpl4">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table [service] = "serviceImpl5"
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Я пытаюсь отойти от этого подхода, потому что он кажется плохим.

Я хочу что-то вроде этого:

@Component({
  providers: [
    { provide: BaseService, useClass: ImplementationService1 },
    { provide: BaseService, useClass: ImplementationService2 },
    { provide: BaseService, useClass: ImplementationService3 },
    { provide: BaseService, useClass: ImplementationService4 },
    { provide: BaseService, useClass: ImplementationService5 }]
})

но Angular, очевидно, не знает, куда внедрить каждый из соответствующих сервисов.

Возможно, важно отметить, что все реализации extend класса BaseService представляют собой абстрактный класс с реализацией методов по умолчанию, которые в большинстве случаев не меняются, по крайней мере, для большинства реализаций.

Вопрос

Есть ли способ сообщить Angular, какой сервис следует внедрить, или мне следует придерживаться текущего подхода (внедрение сервиса в качестве аргумента Input())? Неужели нынешний подход настолько плох?

Обновлено:

CustomTable<T> и BaseService<T> являются общими классами, кроме того, BaseService<T> — абстрактный класс без декоратора @Injectable. Все ImplementationService расширяют BaseService конкретной моделью как T.

Согласно обсуждениям в ответах: было бы полезно знать, почему ваши услуги должны быть разных классов? Разве это не может быть один и тот же класс обслуживания с разными настройками для каждого экземпляра? Это сделало бы все намного проще.

JSON Derulo 16.07.2024 13:26

@JSONDerulo ну, не совсем. Некоторые из них могут быть такими, как вы предложили. Но некоторые из них требуют дополнительной обработки данных и переопределения некоторых реализаций по умолчанию. Поэтому архитектура построена таким образом, что каждая из моделей имеет свой Сервис.

Sulejman 16.07.2024 13:39

Все пытаются ответить на ваш вопрос, но никто не говорит вам правду: вход – это путь. Я не понимаю, почему вы думаете, что это плохой подход, но именно поэтому входные данные существуют!

MGX 16.07.2024 15:13

@MGX Хотя я согласен, что Input() — это довольно простой способ решить эту проблему, в некоторых случаях вам может потребоваться, чтобы Служба была доступна в конструкторе Компонента. В противном случае вам просто придется дождаться вызова ngOnInit() и выполнить неопределенные проверки в шаблоне перед этим вызовом.

Sulejman 16.07.2024 15:29

@Sulejman, если ваша единственная проблема связана с неопределенными проверками, рассмотрите возможность использования входных данных вместо принятого решения, просто сравните сложность обоих решений...

MGX 16.07.2024 17:54

@MGX Ну, как я уже говорил в посте, я сейчас использую входные данные. Но да, я подумаю о том, чтобы не переходить на этот новый подход, поскольку это кажется ненужной магией оболочки/шаблона. Спасибо за вклад, хотя я действительно думал, что с самого начала сделал это плохо.

Sulejman 16.07.2024 18:49
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
1
6
60
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Почему бы вам не добавить службу в providers компонента app-custom-table, чтобы служба создавалась для каждого экземпляра компонента? Вместо передачи сервисов через входы вы можете просто внедрить и использовать сервис непосредственно в дочернем компоненте, и это будет отдельный экземпляр сервиса для каждого компонента.

@Component({
  selector: 'app-custom-table',
  // this will create a new instance of the service per component instance
  providers: [{ provide: BaseService, useClass: ImplementationService }],
})
export class CustomTableComponent {
  constructor(private service: BaseService) {}

  columnButtonClick(event: unknown) {
    this.service.columnClicked(event);
  }
}

См. руководство по внедрению зависимостей .

Если вы хотите вызывать службу не каждый раз, а по условию, вы можете добавить в компонент входные данные для управления этим.

Та же проблема, что и в другом ответе.

Sulejman 16.07.2024 13:09

@Sulejman Сулейман, это важная информация, которая должна быть частью вопроса. Однако в данном случае это не имеет большого значения, просто создайте один сервис и добавьте его к поставщикам компонентов. Обновлю свой ответ.

JSON Derulo 16.07.2024 13:13

ОБНОВЛЯТЬ:

В родительском сервисе вместо BaseService используйте разные экземпляры.

@Component({
  providers: [
    ImplementationService1,
    ImplementationService2,
    ImplementationService3,
    ImplementationService4,
    ImplementationService5,
})
export class SomeComponent {

    constructor(
      private impSer1: ImplementationService1
      private impSer2: ImplementationService2
      private impSer3: ImplementationService3
      private impSer4: ImplementationService4
      private impSer5: ImplementationService5
    ) {}

Добавьте их в HTML.

<mat-tab-group>
  <mat-tab *ngIf = "sees1">
    <app-custom-table [service] = "impSer1"
      (columnButtonClick) = "service1.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees2">
    <app-custom-table [service] = "impSer2">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees3">
    <app-custom-table [service] = "impSer3">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees4">
    <app-custom-table [service] = "impSer4">
    </app-custom-table>
  </mat-tab>
  <mat-tab *ngIf = "sees5">
    <app-custom-table [service] = "impSer5"
      (columnButtonClick) = "service5.columnClicked($event)">
    </app-custom-table>
  </mat-tab>
</mat-tab-group>

Во-первых, должен быть более простой способ решить то, что вы хотите.

Но на данный момент это мое предложение.

Добавьте сервис в массив поставщиков компонента app-custom-table.

@Component({
  selector: 'app-custom-table',
  ...
  providers: [{ provide: BaseService, useClass: ImplementationService },],
})
export class CustomTable {

Теперь все дочерние компоненты имеют уникальный экземпляр.

Затем возьмите дочерний элемент представления от родительского к дочерним компонентам, чтобы получить текущий экземпляр, для этого вы можете использовать геттер.

...
@ViewChild(CustomTable) customTable: CustomTable;

get baseServiceInstance() {
    return this.customTable?.baseService;
}

Примечание: услуга будет доступна только ngAfterViewInit, поэтому начните использовать эту логику оттуда, а не ngOnInit или constructor.

Предложенное решение не работает, поскольку я заявил, что BaseService — это абстрактный класс. Таким образом, помещение его в массив поставщиков приведет к следующему: Cannot assign an abstract constructor type to a non-abstract constructor type.

Sulejman 16.07.2024 13:08

@Sulejman обновил мой ответ, вам нужен только один сервис, экземпляры создаются автоматически, когда вы добавляете их в массив поставщиков.

Naren Murali 16.07.2024 13:10

Хорошо, я немного отредактировал вопрос, так как вы могли его пропустить. Обратите внимание, что все реализации на самом деле являются разными сервисами. Я не могу сделать то, что вы предлагаете, потому что все ImplementationService разные. И не существует ни одного (скажем) ImplementationService1, от которого все остальные могли бы наследовать, чтобы создать этот ImplementationService, который вы предлагаете мне использовать в качестве реализации BaseService.

Sulejman 16.07.2024 13:15

@Sulejman Тогда почему бы вам не предоставить их как есть, без базового сервиса, здесь вы предоставляете один сервис BaseService с несколькими реализациями, что не имеет смысла

Naren Murali 16.07.2024 13:17

Подумайте о BaseService как об интерфейсном сервисе, который я использую в конструкторе CustomTable. Я не совсем понимаю, как вы предлагаете мне "предоставить их как есть" (может быть, вы можете дать на это другой ответ), потому что в конструкторе Custom table у меня это определено так: constructor(private service: BaseService) {}

Sulejman 16.07.2024 13:22

@Sulejman обновил мой ответ, я не думаю, что вам нужно BaseService, если вы хотите расширить возможности использования BaseService, например export class ImplementationService1 extends BaseService {. Но при использовании в провайдерах используйте их напрямую. Вам следует использовать услугу напрямую из @Input, а не из private service: BaseService.

Naren Murali 16.07.2024 13:22

Итак, мое исходное/существующее решение (то есть с Input) является допустимым способом внедрения сервиса? Я имею в виду, что хотел это изменить, потому что мне казалось, что сервисы должны быть всегда доступны в конструкторе. В любом случае спасибо за ваш вклад.

Sulejman 16.07.2024 13:35

@Sulejman Вы можете это сделать, но это потребует дополнительных затрат на добавление компонента-оболочки, который меняет поставщика, это просто выглядит некрасиво, пожалуйста, четко объясните исходную проблему, возможно, есть более простой способ без такого количества экземпляров службы.

Naren Murali 16.07.2024 13:36

Давайте продолжим обсуждение в чате.

Sulejman 16.07.2024 13:49
Ответ принят как подходящий

Я старался абстрагировать решение как можно более абстрактно, однако есть предел тому, насколько абстрактные вещи могут оказаться.

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

Благодаря приведенному ниже решению, которое можно найти в этом рабочем stackblitz, конфигурация была перенесена в токен внедрения, а пользовательские компоненты, таким образом, используют DI для выполнения вашей работы.

Идея решения заключается в том, чтобы

  • Уменьшите загрязнение от основного компонента-хозяина. Внедрение 6 сервисов, которые будут переданы в качестве входных данных в общий компонент таблицы, может привести к созданию уродливого и неподдерживаемого кода.
  • Ваша многократно используемая таблица может использовать DI для определения экземпляра службы, который ей необходимо использовать.
  • В вашей многоразовой таблице больше нет ненужных полей ввода, которые не связаны с общей функциональностью таблицы.
  • Введение компонентов-оболочек обрабатывает конфигурацию DI, которая будет использоваться его дочерними элементами. Кроме того, название компонента уже намекает на то, какие данные

Аннотация BaseService, согласно вашему требованию, не может быть внедрена.

export abstract class BaseService {

  abstract getData():string;
}

Сервис 1 и Сервис 2, расширение/реализация абстрактного сервиса.

@Injectable({ providedIn: 'root' })
export class OneService extends BaseService {
  override getData(): string {
    return 'One Service';
  }
}

@Injectable({providedIn:'root'})
export class TwoService extends BaseService {
  override getData(): string {
    return 'Two Service';
  }
}

Токен инъекции + заводская функция

export let ServiceToken :InjectionToken<BaseService> = new InjectionToken('service-injection');

export let injectionFactory = (key:string) => {
  switch(key) {
    case "1":
      return inject(OneService);
    case "2":
      return inject(TwoService);
     default:
      throw new Error('not-supported');
  }
};

CustomTable, многоразовый, согласно вашему требованию

@Component({
 standalone:true,
 selector:'my-table',
 template:`
    <div>My Table {{key}}</div>
 `,
})
export class TableComponent {

    constructor(@Inject(ServiceToken) public serivce:BaseService){};

    get key():string {
      return this.serivce.getData();
    }
}

Компоненты-оболочки для таблицы, которые внедряют вашу конкретную службу, которая будет использоваться многоразовой таблицей.

@Component({
  standalone: true,
  selector: 'one-table',
  imports: [TableComponent],
  providers: [{ provide: ServiceToken, useExisting: OneService }],
  template: `
   <my-table>
 `,
})
export class OneTableComponent {}

@Component({
  standalone: true,
  selector: 'two-table',
  imports: [TableComponent],
  providers: [{ provide: ServiceToken, useExisting: TwoService }],,
  template: `
    <my-table>
 `,
})
export class TwoTableComponent {}

Это, пожалуй, самое близкое к решению, которое уже упоминалось . Кроме того, я создал еще одну версию без ServiceToken и InjectionFactory, которая тоже работает. Можете ли вы объяснить причину, по которой вы добавили это вместо того, чтобы просто сделать это, как в моей вилке?

Sulejman 16.07.2024 15:24

В некоторых комментариях/обсуждениях я читал, что вы не можете внедрить абстрактную службу и получаете сообщение об ошибке, поэтому я выбрал другое решение. Однако, если внедрение абстрактной службы работает для вашего реального решения, это лучше. Код становится намного проще

JeanPaul A. 16.07.2024 15:45

Я имею в виду, что вы не можете внедрить абстрактную службу напрямую (например: providers: [BaseService]), но вы можете предоставить реализацию (OneService или TwoService) для абстрактной службы через массив providers (например, providers: [{provide: BaseService, useClass: OneService}]). В этом случае abstract BaseService, определенный в конструкторе TableComponent, просто заполняется OneService или TwoService соответственно, поскольку они оба extend BaseService

Sulejman 16.07.2024 15:58

Другие вопросы по теме