Vererbung und Abhängigkeitsinjektion

97

Ich habe eine Reihe von angle2-Komponenten, die alle einen Service erhalten sollten. Mein erster Gedanke war, dass es am besten wäre, eine Superklasse zu schaffen und den Service dort zu injizieren. Jede meiner Komponenten würde dann diese Oberklasse erweitern, aber dieser Ansatz funktioniert nicht.

Vereinfachtes Beispiel:

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
  }
}

Ich könnte dies lösen, indem ich MyServicein jede einzelne Komponente einspritze und dieses Argument für den super()Aufruf verwende, aber das ist definitiv eine Art Absurdität.

Wie organisiere ich meine Komponenten richtig, damit sie einen Service von der Superklasse erben?

maxhb
quelle
Dies ist kein Duplikat. Die Frage, auf die verwiesen wird, ist, wie eine DERIVED-Klasse erstellt werden kann, die auf einen Dienst zugreifen kann, der von einer bereits definierten Superklasse injiziert wurde. Meine Frage ist, wie man eine SUPER-Klasse erstellt, die einen Dienst an abgeleitete Klassen erbt. Es ist einfach umgekehrt.
Maxhb
Ihre Antwort (in Ihrer Frage inline) ergibt für mich keinen Sinn. Auf diese Weise erstellen Sie einen Injektor, der unabhängig von dem Injektor ist, den Angular für Ihre Anwendung verwendet. Die Verwendung new MyService()anstelle der Injektion liefert genau das gleiche Ergebnis (außer effizienter). Wenn Sie dieselbe Dienstinstanz für verschiedene Dienste und / oder Komponenten freigeben möchten, funktioniert dies nicht. Jede Klasse erhält eine andere MyServiceInstanz.
Günter Zöchbauer
Sie haben völlig Recht, mein Code wird viele Instanzen von generieren myService. Es wurde eine Lösung gefunden, die dies vermeidet, aber den abgeleiteten Klassen mehr Code hinzufügt ...
maxhb
Das Injizieren des Injektors ist nur dann eine Verbesserung, wenn mehrere verschiedene Dienste an vielen Stellen injiziert werden müssen. Sie können auch einen Dienst einfügen, der Abhängigkeiten zu anderen Diensten aufweist, und diese mithilfe eines Getters (oder einer Methode) bereitstellen. Auf diese Weise müssen Sie nur einen Dienst einfügen, können jedoch eine Reihe von Diensten verwenden. Ihre Lösung und meine vorgeschlagene Alternative haben beide den Nachteil, dass sie es schwieriger machen zu erkennen, welche Klasse von welchem ​​Service abhängt. Ich möchte lieber Tools (wie Live-Vorlagen in WebStorm) erstellen, die es einfacher machen, den Boilerplate-Code zu erstellen und Abhängigkeiten explizit zu beschreiben
Günter Zöchbauer

Antworten:

72

Ich könnte dies lösen, indem ich MyService in jede einzelne Komponente einfüge und dieses Argument für den Aufruf von super () verwende, aber das ist definitiv absurd.

Es ist nicht absurd. So funktionieren Konstruktoren und Konstruktorinjektion.

Jede injizierbare Klasse muss die Abhängigkeiten als Konstruktorparameter deklarieren. Wenn die Oberklasse auch Abhängigkeiten aufweist, müssen diese ebenfalls im Konstruktor der Unterklasse aufgelistet und mit dem super(dep1, dep2)Aufruf an die Oberklasse weitergeleitet werden .

Das Umgeben eines Injektors und das Erwerben von Abhängigkeiten hat zwangsläufig schwerwiegende Nachteile.

Es verbirgt Abhängigkeiten, die das Lesen von Code erschweren.
Es verstößt gegen die Erwartungen eines Menschen, der mit der Funktionsweise von Angular2 DI vertraut ist.
Es unterbricht die Offline-Kompilierung, die statischen Code generiert, um deklarativen und imperativen DI zu ersetzen, um die Leistung zu verbessern und die Codegröße zu reduzieren.

Günter Zöchbauer
quelle
4
Nur um es klar zu machen: Ich brauche es ÜBERALL. Der Versuch, diese Abhängigkeit in meine Superklasse zu verschieben, damit JEDE abgeleitete Klasse auf den Dienst zugreifen kann, ohne sie einzeln in jede abgeleitete Klasse einfügen zu müssen.
Maxhb
9
Die Antwort auf seine eigene Frage ist ein hässlicher Hack. Die Frage zeigt bereits, wie es gemacht werden sollte. Ich habe ein bisschen mehr ausgearbeitet.
Günter Zöchbauer
7
Diese Antwort ist richtig. Das OP beantwortete seine eigene Frage, brach dabei jedoch viele Konventionen. Dass Sie die tatsächlichen Nachteile aufgelistet haben, ist ebenfalls hilfreich und ich werde dafür bürgen - ich habe das Gleiche gedacht.
Dudewad
6
Ich möchte (und werde) diese Antwort wirklich über den "Hack" des OP verwenden. Aber ich muss sagen, dass dies alles andere als trocken erscheint und sehr schmerzhaft ist, wenn ich eine Abhängigkeit in der Basisklasse hinzufügen möchte. Ich musste nur mehr superals 20 Klassen CTOR-Injektionen (und die entsprechenden Anrufe) hinzufügen, und diese Zahl wird in Zukunft nur noch zunehmen. Also zwei Dinge: 1) Ich würde es hassen, wenn eine "große Codebasis" dies tun würde; und 2) Gott sei Dank für vim qund vscodectrl+.
5
Nur weil es unpraktisch ist, heißt das nicht, dass es eine schlechte Praxis ist. Konstruktoren sind unpraktisch, da es sehr schwierig ist, die Objektinitialisierung zuverlässig durchzuführen. Ich würde behaupten, dass die schlechtere Praxis darin besteht, einen Dienst zu erstellen, der "eine Basisklasse benötigt, die 15 Dienste injiziert und von 6 geerbt wird".
Günter Zöchbauer
64

Die aktualisierte Lösung verhindert, dass mithilfe des globalen Injektors mehrere Instanzen von myService generiert werden.

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();
  }
}

Dadurch wird sichergestellt, dass MyService in jeder Klasse verwendet werden kann, die AbstractComponent erweitert, ohne dass MyService in jede abgeleitete Klasse eingefügt werden muss.

Diese Lösung hat einige Nachteile (siehe Kommentar von @ Günter Zöchbauer unter meiner ursprünglichen Frage):

  • Das Injizieren des globalen Injektors ist nur dann eine Verbesserung, wenn mehrere verschiedene Dienste an vielen Stellen injiziert werden müssen. Wenn Sie nur einen gemeinsamen Dienst haben, ist es wahrscheinlich besser / einfacher, diesen Dienst in die abgeleiteten Klassen einzufügen.
  • Meine Lösung und seine vorgeschlagene Alternative haben beide den Nachteil, dass sie es schwieriger machen zu erkennen, welche Klasse von welchem ​​Dienst abhängt.

Für eine sehr gut geschriebene Erklärung von Dependency Injection in Angular2 diese Blog - Post sehen , die mir sehr geholfen , das Problem zu lösen: http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html

maxhb
quelle
7
Dies macht es jedoch ziemlich schwer zu verstehen, welche Dienste tatsächlich injiziert werden.
Simon Dufour
Sollte es nicht this.myServiceA = injector.get(MyServiceA);etc sein?
Jenson-Button-Event
9
@ Gunter Zochbauers Antwort ist die richtige. Dies ist nicht der richtige Weg, um dies zu tun, und es werden viele Winkelkonventionen gebrochen. Es mag einfacher sein, all diese Injektionsaufrufe zu codieren, ist jedoch ein "Schmerz". Wenn Sie jedoch darauf verzichten möchten, Konstruktorcode schreiben zu müssen, um eine große Codebasis aufrechtzuerhalten, schießen Sie sich selbst in den Fuß. Diese Lösung ist nicht skalierbar, IMO, und wird später viele verwirrende Fehler verursachen.
Dudewad
3
Es besteht nicht das Risiko, dass mehrere Instanzen desselben Dienstes vorhanden sind. Sie müssen lediglich einen Dienst im Stammverzeichnis Ihrer Anwendung bereitstellen, um zu verhindern, dass mehrere Instanzen in verschiedenen Zweigen der Anwendung auftreten. Durch die Weitergabe von Diensten an die Vererbungsänderung werden keine neuen Instanzen der Klassen erstellt. @ Gunter Zochbauers Antwort ist richtig.
Ktamlyn
@maxhb Haben Sie jemals eine Injectorglobale Erweiterung untersucht , um zu vermeiden, dass Parameter verkettet werden müssen AbstractComponent? fwiw, ich denke, dass das Einfügen von Abhängigkeiten in eine weit verbreitete Basisklasse, um eine unordentliche Verkettung von Konstruktoren zu vermeiden, eine absolut gültige Ausnahme von der üblichen Regel ist.
Quentin-Starin
4

Anstatt alle Dienste manuell zu injizieren, habe ich eine Klasse erstellt, die die Dienste bereitstellt, z. B. werden die Dienste injiziert. Diese Klasse wird dann in die abgeleiteten Klassen eingefügt und an die Basisklasse weitergegeben.

Abgeleitete Klasse:

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

Basisklasse:

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

Dienstleistungsklasse:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}
Leukipp
quelle
3
Das Problem hierbei ist, dass Sie das Risiko eingehen, einen "Junk Drawer" -Dienst zu erstellen, der im Wesentlichen nur ein Proxy für den Injector-Dienst ist.
kpup
1

Anstatt einen Dienst einzufügen, der alle anderen Dienste als Abhängigkeiten hat, wie folgt:

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
    }
}

Ich würde diesen zusätzlichen Schritt überspringen und einfach alle Dienste in die BaseComponent einfügen, wie folgt:

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

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

Diese Technik setzt zwei Dinge voraus:

  1. Ihr Anliegen bezieht sich ausschließlich auf die Vererbung von Komponenten. Der Grund, warum Sie auf diese Frage gestoßen sind, ist höchstwahrscheinlich die überwältigende Menge an nicht trockenem (WET?) Code, die Sie in jeder abgeleiteten Klasse wiederholen müssen. Wenn Sie die Vorteile eines einzigen Einstiegspunkts für alle Ihre Komponenten und Services nutzen möchten, müssen Sie den zusätzlichen Schritt ausführen.

  2. Jede Komponente erweitert die BaseComponent

Es gibt auch einen Nachteil, wenn Sie den Konstruktor einer abgeleiteten Klasse verwenden, da Sie super()alle Abhängigkeiten aufrufen und übergeben müssen. Obwohl ich keinen Anwendungsfall sehe, der die Verwendung von constructoranstelle von erfordert ngOnInit, ist es durchaus möglich, dass ein solcher Anwendungsfall existiert.

maximiert
quelle
2
Die Basisklasse hat dann Abhängigkeiten von allen Diensten, die eines ihrer Kinder benötigt. ChildComponentA braucht ServiceA? Nun bekommt ChildComponentB auch ServiceA.
Knallfrosch
0

Wenn die übergeordnete Klasse vom Plug-In eines Drittanbieters stammt (und Sie die Quelle nicht ändern können), können Sie Folgendes tun:

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

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

oder am besten (nur ein Parameter im Konstruktor bleiben):

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
quelle
0

Soweit ich weiß, müssen Sie es zuerst instanziieren, um von der Basisklasse zu erben. Um es zu instanziieren, müssen Sie die vom Konstruktor erforderlichen Parameter übergeben, damit Sie sie über einen super () -Aufruf vom Kind zum Elternteil übergeben können, damit dies sinnvoll ist. Der Injektor ist natürlich eine weitere praktikable Lösung.

ihorbond
quelle
0

Hässlicher Hack

Vor einiger Zeit möchte ein Teil meines Kunden zwei große Winkelprojekte bis gestern verbinden (Winkel v4 in Winkel v8). Project v4 verwendet die BaseView-Klasse für jede Komponente und enthält eine tr(key)Methode für Übersetzungen (in v8 verwende ich ng-translate). Um zu vermeiden, dass das Übersetzungssystem gewechselt und Hunderte von Vorlagen (in Version 4) bearbeitet oder das Übersetzungssystem 2 parallel eingerichtet werden, verwende ich folgenden hässlichen Hack (ich bin nicht stolz darauf) - in der AppModuleKlasse füge ich folgenden Konstruktor hinzu:

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

und jetzt können AbstractComponentSie verwenden

export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}
Kamil Kiełczewski
quelle