Wstrzykiwanie dziedziczenia i zależności

99

Mam zestaw komponentów angular2, które powinny zostać wstrzyknięte. Moja pierwsza myśl była taka, że ​​najlepiej byłoby stworzyć superklasę i wstrzyknąć tam usługę. Każdy z moich komponentów rozszerzyłby wtedy tę superklasę, ale to podejście nie działa.

Uproszczony przykład:

export class AbstractComponent {
  constructor(private myservice: MyService) {
    // Inject the service I need for all components
  }
}

export MyComponent extends AbstractComponent {
  constructor(private anotherService: AnotherService) {
    super(); // This gives an error as super constructor needs an argument
  }
}

Mógłbym to rozwiązać, wstrzykując MyServicekażdy komponent i używając tego argumentu do super()wywołania, ale to z pewnością jakiś absurd.

Jak poprawnie zorganizować moje komponenty, aby dziedziczyły usługę z superklasy?

maxhb
źródło
To nie jest duplikat. Przywoływane pytanie dotyczy tego, jak skonstruować klasę DERIVED, która może uzyskać dostęp do usługi wstrzykniętej przez już zdefiniowaną superklasę. Moje pytanie dotyczy tego, jak skonstruować klasę SUPER, która dziedziczy usługę do klas pochodnych. Po prostu jest na odwrót.
maxhb
Twoja odpowiedź (zawarta w pytaniu) nie ma dla mnie sensu. W ten sposób utworzysz wtryskiwacz, który jest niezależny od wtryskiwacza używanego przez Angular w twojej aplikacji. Używanie new MyService()zamiast wstrzykiwania daje dokładnie ten sam wynik (z wyjątkiem bardziej wydajnego). Jeśli chcesz udostępniać tę samą instancję usługi w różnych usługach i / lub składnikach, to nie zadziała. Każda klasa otrzyma inną MyServiceinstancję.
Günter Zöchbauer
Masz całkowitą rację, mój kod wygeneruje wiele instancji myService. Znaleziono rozwiązanie, które pozwala uniknąć tego, ale dodaje więcej kodu do klas pochodnych ...
maxhb
Wstrzyknięcie wtryskiwacza jest poprawą tylko wtedy, gdy istnieje kilka różnych usług, które muszą być wstrzykiwane w wielu miejscach. Możesz także wstrzyknąć usługę, która ma zależności od innych usług i udostępnić je za pomocą metody pobierającej (lub metody). W ten sposób wystarczy wstrzyknąć jedną usługę, ale można korzystać z zestawu usług. Twoje rozwiązanie i moja proponowana alternatywa mają tę wadę, że utrudniają dostrzeżenie, jaka klasa zależy od usługi. Wolałbym raczej tworzyć narzędzia (takie jak szablony na żywo w WebStorm), które ułatwiają tworzenie standardowego kodu i jasno określają zależności
Günter Zöchbauer

Odpowiedzi:

73

Mógłbym rozwiązać ten problem, wstrzykując MyService do każdego komponentu i używając tego argumentu dla wywołania super (), ale to zdecydowanie jakiś absurd.

To nie jest absurd. Tak działają konstruktory i iniekcja konstruktora.

Każda klasa, którą można wstrzykiwać, musi zadeklarować zależności jako parametry konstruktora, a jeśli nadklasa również ma zależności, muszą one również zostać wymienione w konstruktorze podklasy i przesłane wraz z super(dep1, dep2)wywołaniem do nadklasy .

Omijanie wtryskiwacza i uzyskiwanie zależności ma bezwzględnie poważne wady.

Ukrywa zależności, co utrudnia odczytanie kodu.
Narusza to oczekiwania osoby zaznajomionej z działaniem Angular2 DI.
Przerywa kompilację w trybie offline, która generuje kod statyczny w celu zastąpienia deklaratywnego i imperatywnego DI w celu poprawy wydajności i zmniejszenia rozmiaru kodu.

Günter Zöchbauer
źródło
5
Żeby było jasne: potrzebuję tego WSZĘDZIE. Próbuję przenieść tę zależność do mojej super klasy, aby KAŻDA klasa pochodna mogła uzyskać dostęp do usługi bez konieczności wstrzykiwania jej indywidualnie do każdej klasy pochodnej.
maxhb
9
Odpowiedź na jego własne pytanie to brzydki hack. Pytanie już pokazuje, jak należy to zrobić. Opracowałem trochę więcej.
Günter Zöchbauer
7
Ta odpowiedź jest poprawna. PO odpowiedział na swoje pytanie, ale złamał przy tym wiele konwencji. To, że wymieniłeś rzeczywiste wady, jest również pomocne i ręcę za to - myślałem o tym samym.
dudewad
6
Naprawdę chcę (i nadal używam) tej odpowiedzi zamiast „hackowania” PO. Ale muszę powiedzieć, że wydaje się to dalekie od SUCHEGO i jest bardzo bolesne, gdy chcę dodać zależność w klasie bazowej. Musiałem tylko dodać zastrzyki ctora (i odpowiadające im superwywołania) do około 20+ klas i ta liczba będzie rosła w przyszłości. Tak więc dwie rzeczy: 1) nie chciałbym widzieć, jak robi to „duża baza kodu”; i 2) Dzięki Bogu za vim qi vscodectrl+.
6
To, że jest niewygodne, nie oznacza, że ​​jest to zła praktyka. Konstruktorzy są niewygodni, ponieważ bardzo trudno jest niezawodnie przeprowadzić inicjalizację obiektu. Twierdzę, że najgorszą praktyką jest tworzenie usługi, która wymaga „wstrzyknięcia klasy bazowej 15 usług i jest dziedziczona przez 6”.
Günter Zöchbauer
65

Zaktualizowane rozwiązanie zapobiega generowaniu wielu instancji myService przy użyciu globalnego wtryskiwacza.

import {Injector} from '@angular/core';
import {MyServiceA} from './myServiceA';
import {MyServiceB} from './myServiceB';
import {MyServiceC} from './myServiceC';

export class AbstractComponent {
  protected myServiceA:MyServiceA;
  protected myServiceB:MyServiceB;
  protected myServiceC:MyServiceC;

  constructor(injector: Injector) {
    this.settingsServiceA = injector.get(MyServiceA);
    this.settingsServiceB = injector.get(MyServiceB);
    this.settingsServiceB = injector.get(MyServiceC);
  }
}

export MyComponent extends AbstractComponent {
  constructor(
    private anotherService: AnotherService,
    injector: Injector
  ) {
    super(injector);

    this.myServiceA.JustCallSomeMethod();
    this.myServiceB.JustCallAnotherMethod();
    this.myServiceC.JustOneMoreMethod();
  }
}

Zapewni to, że MyService będzie można używać w dowolnej klasie, która rozszerza AbstractComponent bez konieczności wstrzykiwania MyService w każdej klasie pochodnej.

Jest kilka wad tego rozwiązania (zobacz komentarz @ Günter Zöchbauer pod moim oryginalnym pytaniem):

  • Wstrzyknięcie globalnego wtryskiwacza jest poprawą tylko wtedy, gdy istnieje kilka różnych usług, które muszą być wstrzykiwane w wielu miejscach. Jeśli masz tylko jedną usługę udostępnioną, prawdopodobnie lepiej / łatwiej jest wstrzyknąć tę usługę w ramach klas pochodnych
  • Zarówno moje rozwiązanie, jak i proponowana przez niego alternatywa mają tę wadę, że utrudniają dostrzeżenie, która klasa zależy od usługi.

Bardzo dobrze napisane wyjaśnienie wstrzykiwania zależności w Angular2 znajduje się w tym poście na blogu, który bardzo pomógł mi rozwiązać problem: http://blog.hardtram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html

maxhb
źródło
7
To sprawia, że ​​dość trudno jest zrozumieć, jakie usługi są faktycznie wstrzykiwane.
Simon Dufour
1
Nie powinno być this.myServiceA = injector.get(MyServiceA);itd.?
wydarzenie jenson-button
10
Odpowiedź @Guntera Zochbauera jest prawidłowa. To nie jest właściwy sposób, aby to zrobić i łamie wiele kątowych konwencji. Może być prostsze w tym, że kodowanie wszystkich tych wywołań iniekcji jest „uciążliwe”, ale jeśli chcesz poświęcić konieczność pisania kodu konstruktora w celu utrzymania dużej bazy kodu, to strzelasz sobie w stopę. To rozwiązanie nie jest skalowalne, IMO, i spowoduje wiele mylących błędów w przyszłości.
dudewad
3
Nie ma ryzyka wystąpienia wielu wystąpień tej samej usługi. Musisz po prostu udostępnić usługę w katalogu głównym aplikacji, aby zapobiec wielu wystąpieniom, które mogą wystąpić w różnych gałęziach aplikacji. Przekazywanie usług w dół zmiany dziedziczenia nie tworzy nowych instancji klas. Odpowiedź @Guntera Zochbauera jest poprawna.
ktamlyn
@maxhb Czy kiedykolwiek badałeś rozszerzanie Injectorglobalne, aby uniknąć konieczności łączenia jakichkolwiek parametrów AbstractComponent? fwiw, myślę, że wstrzykiwanie zależności właściwości do szeroko stosowanej klasy bazowej w celu uniknięcia niechlujnego tworzenia łańcuchów konstruktorów jest całkowicie poprawnym wyjątkiem od zwykłej reguły.
quentin-starin
4

Zamiast ręcznie wstrzykiwać wszystkie usługi, utworzyłem klasę udostępniającą usługi, np. Pobiera ona wstrzyknięte usługi. Ta klasa jest następnie wstrzykiwana do klas pochodnych i przekazywana do klasy bazowej.

Klasy pochodnej:

@Component({
    ...
    providers: [ProviderService]
})
export class DerivedComponent extends BaseComponent {
    constructor(protected providerService: ProviderService) {
        super(providerService);
    }
}

Klasa podstawowa:

export class BaseComponent {
    constructor(protected providerService: ProviderService) {
        // do something with providerService
    }
}

Klasa świadcząca usługi:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}
Leukipp
źródło
3
Problem polega na tym, że ryzykujesz utworzenie usługi „szuflady śmieci”, która jest po prostu proxy dla usługi Injector.
kpup
1

Zamiast wstrzykiwać usługę, która ma wszystkie inne usługi jako zależności, na przykład:

class ProviderService {
    constructor(private service1: Service1, private service2: Service2) {}
}

class BaseComponent {
    constructor(protected providerService: ProviderService) {}

    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

Pominąłbym ten dodatkowy krok i po prostu dodałbym wstrzyknięcie wszystkich usług w BaseComponent, na przykład:

class BaseComponent {
    constructor(protected service1: Service1, protected service2: Service2) {}
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        this.service1;
        this.service2;
    }
}

Ta technika zakłada 2 rzeczy:

  1. Twoje obawy są całkowicie związane z dziedziczeniem składników. Najprawdopodobniej powodem, dla którego trafiłeś na to pytanie, jest przytłaczająca ilość kodu nieosuszonego (WET?), Który musisz powtórzyć w każdej klasie pochodnej. Jeśli chcesz skorzystać z jednego punktu wejścia dla wszystkich swoich składników i usług , musisz wykonać dodatkowy krok.

  2. Każdy składnik rozszerza BaseComponent

Istnieje również wada, jeśli zdecydujesz się użyć konstruktora klasy pochodnej, ponieważ będziesz musiał wywołać super()i przekazać wszystkie zależności. Chociaż tak naprawdę nie widzę przypadku użycia, który wymaga użycia constructorzamiast ngOnInit, jest całkowicie możliwe, że taki przypadek użycia istnieje.

maximedupre
źródło
2
Klasa bazowa ma wtedy zależności od wszystkich usług, których potrzebuje każda z jej elementów podrzędnych. ChildComponentA potrzebuje usługi A? Cóż, teraz ChildComponentB również otrzymuje ServiceA.
knallfrosch
1

Z tego, co rozumiem, aby dziedziczyć z klasy bazowej, najpierw musisz ją utworzyć. Aby go utworzyć, musisz przekazać jego konstruktorowi wymagane parametry, więc przekazujesz je z dziecka do rodzica za pomocą wywołania super (), więc ma to sens. Innym dobrym rozwiązaniem jest oczywiście wtryskiwacz.

ihorbond
źródło
0

Jeśli klasa nadrzędna została pobrana z wtyczki innej firmy (i nie możesz zmienić źródła), możesz to zrobić:

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  constructor(
    protected injector: Injector,
    private anotherService: AnotherService
  ) {
    super(injector.get(MyService));
  }
}

lub w lepszy sposób (pozostań tylko jeden parametr w konstruktorze):

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  private anotherService: AnotherService;

  constructor(
    protected injector: Injector
  ) {
    super(injector.get(MyService));
    this.anotherService = injector.get(AnotherService);
  }
}
dlnsk
źródło
0

UGLY HACK

Jakiś czas temu niektórzy z moich klientów chcą dołączyć do dwóch BIG angular projektów do wczoraj (angular v4 w angular v8). Projekt v4 używa klasy BaseView dla każdego komponentu i zawiera tr(key)metodę tłumaczenia (w v8 używam ng-translate). Aby więc uniknąć przełączania systemu tłumaczeń i edytować setki szablonów (w v4) lub ustawić równolegle 2 system tłumaczeń, używam poniższego brzydkiego hacka (nie jestem z tego dumny) - w AppModuleklasie dodaję następujący konstruktor:

export class AppModule { 
    constructor(private injector: Injector) {
        window['UglyHackInjector'] = this.injector;
    }
}

a teraz AbstractComponentmożesz użyć

export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}
Kamil Kiełczewski
źródło