Suche
Tutorial: So funktionieren Abhängigkeiten in Angular 2

Angular Pro-Tipps: #3 – Dependency Injection

Aaron Czichon

Im ersten Teil der Angular 2 Pro-Tipps von Aaron Czichon wurden die mit Angular 2 eingeführten Pipes näher beleuchtet. Für den zweiten Teil widmete sich unser Autor ausführlich dem HTTP-Client. Der dritte Teil der Angular 2 Pro-Tipps steht nun ganz im Zeichen der Dependency Injection in Angular 2.

Bisher gelernt

Angular Pro-Tipps von Aaron Czichon

Bisher haben Sie gelernt, wie Sie mit den in Angular 2 eingeführten Pipes umgehen. Diese können genutzt werden, um Daten und Objekte vor der Anzeige in der View und/oder vor dem Rendering zu transformieren und entsprechend zu formatieren. Wenn Sie etwa ein Datum in einem bestimmten Format anzeigen wollen, können Sie hierfür eine Pipe verwenden und müssen nicht das eigentliche Datenobjekt bearbeiten.

Im zweiten Teil wurde der HTTP-Client etwas genauer dargestellt. Sie haben gelernt, wie Sie den neuen Client nutzen können, um Daten von einer Schnittstelle abzurufen und wie das Grundprinzip von Observables funktioniert.

In diesem Teil der Kolumne geht es nun um die Dependency Injection in Angular 2.

Was ist Dependency Injection?

Das Konzept der Dependency Injection sieht vor, Abhängigkeiten von außen in eine Instanz zu übergeben und diese darin zu nutzen. Das Konzept besitzt Ähnlichkeiten mit den aus anderen Programmiersprachen bekannten Singletons. Bei Singletons wird innerhalb der Applikation eine Instanz, wenn diese noch nicht existiert, erzeugt. Diese wird dann an verschiedenen Stellen der Applikation genutzt. Dabei wird immer auf dieselbe Instanz zurückgegriffen, sofern diese noch existiert.

Bei der Dependency Injection in Angular können nun Abhängigkeiten einer Komponente oder eines Services von außen übergeben werden. Diese Komponenten nutzen dann die Instanz der Dependency Injection, um damit zu arbeiten. Dieses Konzept ermöglicht es Ihnen als Entwickler, verschiedene Zugriffe und Zugriffsschichten besser zu kapseln. Beispielsweise können Sie API-Zugriffe über eine Dependency-Injection-Instanz leiten und brauchen nicht in jeder Komponente eine neue Instanz Ihrer API-Zugriffskomponente.

Vergleich zu Angular 1.x

In Angular 1.x wurde die Dependency Injection etwas anders gehandhabt als in der aktuellen Version von Angular. Dort wurde eine Dependency als Parameter der Funktion übergeben und konnte so genutzt werden.

angular.module('myModule', [])
.directive('directiveName', ['myDependencyService',
function(myDependencyService) {
    // ...
}])

Dieses Konzept wurde nicht direkt in die aktuelle Angular-Version übernommen. Dort gibt es eine neue Implementierung, die vieles vereinfacht und mehrere Optionen anbietet.

Application-wide / Module Dependencies

Um nun eine neue Dependency anzulegen, müssen Sie zunächst entscheiden, wo diese Dependency genutzt werden soll. Wenn die Dependency innerhalb der Applikation zur Verfügung stehen soll, müssen Sie eine sogenannte Application-Wide-Dependency anlegen. Dieseimplementieru wird in der AppComponent angelegt und steht der gesamten Applikation zur Verfügung. Wollen Sie eine Dependency nur einem bestimmten Module mit ngModule zur Verfügung stellen, müssen Sie die Dependency in der Module-Klasse definieren.

Application-Wide-Dependencies

Um eine Application-Wide-Dependency anzulegen, benötigen Sie zunächst eine Klasse / einen Service, welcher als Dependency genutzt werden soll. Daher legen Sie zunächst einen neuen Service an, der User-Daten verwalten soll. Nennen Sie diesen Service UserService:

// user-service.ts
/* . . . */

export class UserService {
    private user = {
        name: 'Aaron Czichon',
        city: 'Aalen'
    }
    constructor() { }
    
    getUser() {
        return this.user;
    }
    
    updateCity(city: string) {
        this.user.city = city;
    }
}

Wechseln Sie nun zu Ihrer app.component.ts. Diese sollte aktuell bei einem neuen Angular-Projekt in etwa so aussehen:

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html'
})
export class AppComponent {
/* . . . */
}

Um nun den UserService als Dependency nutzen zu können, muss dieser in der app.component.ts bekannt gemacht werden. Mit einem import und der Deklaration als Provider ist das schnell erledigt:

// app.component.ts
import { Component } from '@angular/core';
import { UserService } from './user-service';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html',
  providers: [UserService]
})
export class AppComponent {
    constructor(private userSrv: UserService) { }
/* . . . */
}

Nun steht der UserService in der Applikation zur Verfügung. Überall dort, wo nun in unserer Applikation die Dependency eingebunden wird (durch den Import und der Deklaration als Provider), steht die Instanz des Services zur Verfügung. Legen wir nun eine neue Komponente an und nutzen darin die Dependency des UserService:

// user-details.component.ts
import { Component } from '@angular/core';
import { UserService } from './user-service';

@Component({
    selector: 'user-details',
    template: `
        <div>{{user.name}}</div>
        <div>{{user.city}}</div>
    `,
    providers: [UserService]
})
export class UserDetails {
    user: any;
    constructor(private userSrv: UserService) {
        this.user = userSrv.getUser();
    }
}

Damit die User-Details auch angezeigt werden, muss die Komponente der app.component.ts per Import bekannt gemacht werden. Die Anwendung zeigt uns nun folgendes Ergebnis an:

Aaron Czichon
Aalen

Wird nun in der app.component.ts im Konstruktor zum Beispiel das Property city neu gesetzt, wird auch der neue Wert in der user-details.component.ts genutzt:

// app.component.ts
import { Component } from '@angular/core';
import { UserService } from './user-service';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html',
  providers: [UserService]
})
export class AppComponent {
    constructor(private userSrv: UserService) {
        userSrv.updateCity('Heidenheim');
    }
/* . . . */
}

Erweitert man nun die user-detail.component.ts mit einer Refresh-Funktion, kann der neue Wert auch aus der Dependency gelesen werden:

// user-details.component.ts
import { Component } from '@angular/core';
import { UserService } from './user-service';

@Component({
    selector: 'user-details',
    template: `
        <div>{{user.name}}</div>
        <div>{{user.city}}</div>
        <div (click)="refresh()">Werte neu lesen</div>
    `,
    providers: [UserService]
})
export class UserDetails {
    user: any;
    constructor(private userSrv: UserService) {
        this.user = userSrv.getUser();
    }
    
    refresh() {
        this.user = userSrv.getUser();
    }
}

Zunächst ist die Ausgabe weiterhin wie bisher. Da die Anwendung aber im Konstruktor der app.component.ts die Daten der Instanz des UserService ändert, wechselt die Ausgabe bei einem Klick auf Werte neu laden zu:

Aaron Czichon
Heidenheim

Module Dependencies

Da Angular-Applikationen jedoch nicht in einer großen Root Application entwickelt werden (sollten), wird die Applikation in sogenannte Module verteilt. Jedes Modul kann für sich selbst genutzt und weiterverwendet werden. Ein Modul sollte unabhängig von der restlichen Applikation agieren können und auch innerhalb einer anderen Applikation weiter funktionieren. Genaueres dazu in Angular 2 Pro-Tipps: #4 – ngModule.

Um aber dennoch kurz auf die Dependency Injection innerhalb von Modulen einzugehen, hier ein kleines Beispiel:

Um eine Dependency Injection, beispielsweise von unserem UserService, innerhalb eines Moduls nutzen zu können, wird die Instanz in der Modul-Konfiguration importiert. Gehen wir davon aus, wir haben eine neue Angular-Applikation angelegt und möchten den UserService in unserem App-Modul nutzen. Dieses sieht, wenn es neu angelegt wurde, wie folgt aus:

// app.module.ts

/* . . . */
@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent
    /* . . . */
  ],
  providers: [ ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Um nun den UserService innerhalb dieses Moduls als Dependency zu nutzen, wird der Service wieder importiert und in der Modul-Konfiguration angegeben.

// app.module.ts

/* . . . */
import { UserService } from './user-service';

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent
    /* . . . */
  ],
  providers: [ UserService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Da die app.component.ts ein Bestandteil des AppModule ist, brauchen wir dort keine explizite Anweisung zur Erzeugung des Imports:

// app.component.ts
import { Component } from '@angular/core';
import { UserService } from './user-service';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html'
})
export class AppComponent {
    constructor(private userSrv: UserService) {
        userSrv.updateCity('Heidenheim');
    }
/* . . . */
}

Dependencies, die von Dependencies abhängen

Oftmals benötigt man nicht nur eine Dependency innerhalb der Applikation. In vielen Fällen kommt es vor, dass eine Dependency von einer anderen Dependency abhängt. Nehmen wir mal als Beipspiel den UserService. Wenn dieser Service nun die User von einer Web-Resource abfragen möchte, benötigt er eine Dependency zum HTTP-Client.

Der erzeugenden Komponente (oder dem Modul) muss nun klar sein, dass innerhalb des Services weitere Abhängigkeiten vorhanden sind, die aufgelöst werden müssen. Dies weiß aber die Komponente nicht von Beginn an. Um der Komponente dies mitzuteilen, muss der UserService entsprechend mit dem sogenannten @Injectable-Decorator erweitert werden:

// user-service.ts
/* . . . */
import { Injectable } from '@angular/core';

@Injectable()
export class UserService {
    private user = {
        name: 'Aaron Czichon',
        city: 'Aalen'
    }
    constructor() { }
    
    getUser() {
        return this.user;
    }
    
    updateCity(city: string) {
        this.user.city = city;
    }
}

Handling von Dependencies

Es gibt zwei wichtige Decorators, um mit Dependencies noch genauer zu arbeiten. Zum einen den @Host-Decorator und zum anderen den @Optional-Decorator.

Mit dem @Host-Decorator kann einer Komponente oder einem Modul mitgeteilt werden, dass nur innerhalb dieser Komponente oder dem Modul nach einer Instanz der Dependency gesucht werden darf. Parallele oder übergeordnete Instanzen werden dabei nicht beachtet.
Dadurch ist dem Entwickler die Möglichkeit gegeben, mehrere Instanzen einer Dependency zu haben. Es können somit unterschiedliche Kontexte gebildet werden.

Der @Optional-Decorator prüft, ob in der Anwendung / dem Modul eine Instanz der Dependency gefunden wird. Sollte dies nicht der Fall sein, wird aber auch keine Instanz erzeugt (die Dependency bleibt also leer). Kombiniert man nun den @Optional– und den @Host-Decorator, limitiert man somit die Suche nach einer Instanz einer Dependency auf eine Komponente oder ein Modul und deklariert diese aber als optional. Sollte also in einer Komponente oder einem Modul keine Instanz dazu existieren, wird auch keine erzeugt. Somit kann man Fehler vermeiden oder dem Benutzer spezifische Rückmeldungen geben, dass zuvor etwas anderes erledigt werden muss, was dann die entsprechend notwendig gewordene Instanz erzeugt.

Geschrieben von
Aaron Czichon
Aaron Czichon
Aaron Czichon hat im Alter von 14 Jahren mit Software-Entwicklung angefangen und sich PHP und HTML beigebracht. Seit seiner Ausbildung zum Fachinformatiker Anwendungsentwicklung arbeitet er hauptberuflich bei der cellent AG mit Hauptsitz in Stuttgart. Dort ist er als Software-Entwickler im Projektumfeld mit Javascript und .NET tätig. Nebenberuflich ist er als freier Entwickler mit seiner WebAtlas GbR unterwegs und macht seinen IT-Fachwirt um seine Kompetenzen in Unternehmensführung und BWL zu erweitern. Mit Ionic beschäftigt er sich seit dem Erscheinen der 3. Alpha-Version Ende November 2013. Ende 2014 gründete er die deutsche Ionic Community „Ionic Germany“. Er ist Mitglied der Ionic Alpha-Gruppe, welche die aktuellsten Neuerungen und Tools von Ionic zum Testen erhält. Nebenbei ist er auf Konferenzen als Speaker. Dort referiert er überwiegend über das Ionic Framework.
Kommentare
  1. Benjamin Makus2017-01-10 13:28:51

    Der Artikel wirkt nicht sehr durchdacht. Zum einen richtet er sich inhaltlich an Einsteiger, aber ist gleichzeitig ziemlich schwammig, wenig erklärend und teilweise zu kompliziert geschrieben. Hinzu kommen noch kleinere Rechtschreibfehler und uneinheitliche Benamung (z.B. warum heißt die Klasse nicht UserDetailsCOMPONENT?, warum beginnt "Injectable-Decorator" nicht mit einem "@"?).

    Es wird von @Injectable und weiteren Abhängigkeiten geredet, aber im darauf folgenden Codebeispiel ist davon nichts zu sehen.

    Zu @Host und @Optional wäre ein kleines Code Beispiel praktisch gewesen, wenn man diese Decorator schon in einem Text für Anfänger erwähnt. Ich persönlich habe bisher weder @Host noch @Optional verwendet.

    Dafür bin ich aber schon sehr oft mit "multi: true" in Kontakt gekommen, das hier nicht mal nebenbei erwähnt wurde. Dependency Injection besteht schließlich nicht nur aus Singletons.

    Ansonsten fände ich es noch sehr wichtig, die zukünftigen Entwickler über Stateless/Stateful Services aufzuklären. Der Beispielcode suggeriert, dass Stateful Services die Normalität seien, dabei sollte das die Ausnahme sein! (Das vermeidet auf lange Sicht sehr viele Probleme)

  2. Aaron Czichon2017-01-10 14:34:34

    Hallo Benjamin,

    danke für dein Feedback. Was genau würdest du als "schwamming" und "wenig erklärend" bezeichnen?

    Ob die Klasse nun UserDetailsComponent heißt oder UserDetails kommt auf die eigenen Naming-Conventions an. Sollte das für dich einfacher sein, kannst du diese natürlich gerne so benennen.
    Wo genau fehlt der der @Injectable Decorator? Aktuell ist er überall, wo er benötigt wird, gesetzt.

    Zum @Host und @Optional wäre ein Beispiel gut gewesen, stimme ich dir zu.

    Multi-Provider wäre noch eine Idee gewesen, dies in den Artikel mit aufzunehmen.

    Wenn du es interpretierst, dass es hauptsächlich Stateful Services gibt, tut mir das leid. Da stimme ich dir natürlich zu. Stateful Services sollten, sofern möglich, vermieden werden und nur dort eingesetzt werden wo es nötig ist.

  3. Benjamin Makus2017-01-10 15:19:15

    Natürlich kommt es auf die Naming-Conventions an. Aber wenn ich sehe, dass die eine Komponente "AppComponent" heißt und die nächste "UserDetails", dann sieht das nicht mehr nach Naming-Convention aus.
    Das fehlende "@" beim Injectable-Decorator im Fließtext scheinst du ja gefunden zu haben :)

    Mit "schwammig" und "wenig erklärend" meinte ich z.B. der Part über die Singletons, denn eigentlich sind DI und Singleton zwei grundlegend verschiedene Konzepte und haben rein gar nichts gemeinsam (außer dass beide der Kategorie "Entwurfsmuster" angehören): Bei DI geht's um Abhängigkeiten und beim Singleton um Daten-Gateways.
    Hingegen finde ich völlig korrekt zu sagen, dass der Großteil der Abhängigkeiten bei DI aus Singletons besteht. Und dass Singletons andere Abhängigkeiten via DI erhalten können.

    Der "Unterschied" zu AngularJS 1.x ist auch nur eine Halbwahrheit. "constructor" ist schließlich auch nur eine Funktion. Oder anders gesagt: Wenn ich Angular 1 Code mit Typescript schreibe, dann nutze ich auch Klassen und den "constructor" für die Dependencies. Der Unterschied zu Angular 2 sind am Ende eigentlich nur noch die Decorators und Multi-Provider.

    Bei der Sache mit "userSrv.updateCity('Heidenheim')" scheint es nun so als wenn der UserDetails constructor VOR dem AppComponent constructor aufgerufen wird bzw. die UserDetails Komponente dargestellt wird bevor der AppComponent constructor aufgerufen wird?! (schließlich muss man ja laut Text erst den refresh-Button drücken, um die neue Stadt zu sehen).
    Ich beschäftige mich nun seit fast einem Jahr mit Angular2 und kann mir das Verhalten des Codes vom Lesen des Textes nicht erklären.

    Und ich finde das Code-Beispiel zum @Injectable immer noch unpassend. Davor wird von "innerhalb des Services weitere Abhängigkeiten" und "Dependency zum HTTP-Client" gesprochen und im Code ist dann nur ein leerer constructor zu sehen komplett ohne irgendwelche Abhängigkeiten.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.