Angular Services

Wir haben über Module gesprochen - Blöcke aus Applikationslogik (Controller) mit zugehöriger View. Wir haben über Direktiven und Pipes gesprochen, mehr oder weniger Views ohne Controller.

Jetzt fehlen noch Services, also quasi Controller ohne View.

Wir müssen in diesem Zusammenhang auch auf Dependency Injection eingehen. Aber das machen wir etwas später. Zuerst sollten wir uns die Frage stellen, warum man einen Service haben will.

Services sind, wie oben bereits gesagt, gekapselte Logik ohne (direkte) Verbindung zu einer Oberfläche. “Gekapselt” heißt: in einem Service ist alles was zu einer Funktionalität gehört gebündelt. Und das heißt auch: ein Service macht immer genau eine Sache (wie überhaupt jede Klasse natürlich). Das macht es uns leicht, diesen Service zu testen (und über das Thema “Testen”, da müssen wir auch bald einmal reden) und zu verwalten.

Vielleicht holt sich der Service einen Session-Token von einem Backend ab (was übrigens unser Beispiel für heute sein wird). Wie er das macht, das sollte einem Modul, das auf diese Funktionalität angewiesen ist, herzlich egal sein.

Es ist auch vorstellbar, dass ein Login-Service in unterschiedlichen Kontexten unterschiedliche Dinge machen muss. Es gibt also nicht nur einen Login-Service, sondern mehrere Ausprägungen davon.

Und auch das sollte unserer Anwendung egal sein. Die weiß nur: sie braucht eben einen Login-Service um zu funktionieren. Egal welche Variante. Jedem dieser Login-Services aber ist gemein, dass er

  • sich um die Authentifizierung kümmert und…
  • …Session-Daten bereithält, die für alle Teile der Anwendung relevant sind

Und jetzt kommen wir langsam zum Thema Dependency Injection (oder kurz DI)…

Woher weiß das Modul denn dann, welches Service es benutzen soll/darf? Ganz einfach: das muss es nicht wissen, denn das wird ihm von außen vorgegeben. Das Moduls selbst erstellt keine Instanz eines Services, es bekommt eine Referenz übergeben. Und das kann je nach Kontext eben eine andere Geschmacksrichtung sein. So ein Vorgehen nennt sich dann übrigens das Prinzip der losen Kopplung.

Weiterer Vorteil ist: wir haben eine zentrale Instanz der Datenhaltung, die an mehreren Stellen angezapft werden kann, um zum Beispiel aktuelle Session-Daten zu erhalten (wie in etwa “Ist der Benutzer bereits angemeldet?” oder “Wie lautet das Session-Token?”).

Aber irgendwer muss natürlich den Service erstellen. Das ist in Angular der Service-Provider. Die vom Provider bereitgestellte Service-Instanz wird dann, wann immer benötigt, an andere Module weitergegeben.

Fassen wir die Theorie zusammen:

  • Ein Service bietet definierte Funktionalität
  • Ein Provider kennt einen oder mehrere Services
  • Eine Komponente benötigt eine bestimmte Funktionalität
  • Eine Komponente bekommt vom Provider eine Service-Referenz

Zwischen Komponente und Provider steht noch der Injector selbst, also der Framework-Teil, der alle Provider verwaltet und Module und andere Komponenten mit Instanzen der gewünschten Services zusammenbringt.

Schreiben wir mal einen provisorischen Login-Service:

export class LoginService {
  login(loginCredentials: Array<String>) {
    // ...
  }
}

In dieser Klasse erledigen wir die Authentifizierung des Users am Backend, halten Session-Daten vor, lassen Sitzungen auslaufen und so weiter. Diesen LoginService müssen wir im App-Modul registrieren:

@NgModule({
  imports: [ BrowserModule ],
  providers: [ LoginService ],
  // ...
})
export class AppModule {
  // ...
}

Von jetzt an steht der LoginService jeder Komponente zur Verfügung. Ein Beispiel:

import { Component, Inject, OnInit } from '@angular/core';
import { LoginService } from './LoginService.ts';

@Component({
  // ...
})
export class UserComponent {
  constructor(loginService: LoginService) {
    // ...
    loginService.login([ username, password ]);
  }
}

Der LoginService wird automatisch vom DI-Framework aufgelöst und die UserComponent erhält die Instanz des Services im Konstruktor übergeben.

Alternativ wäre auch die Verwendung einer Factory-Methode denkbar:

// ...
export function LoginServiceFactory() {
  if (API_VERSION >= 2) {
    return new LoginService();
  } else {
    return new LegacyLogin();
  }
}

@NgModule({
  providers: [
    { provide: LoginService, useFactory: LoginServiceFactory }
  ]
})

Weiter geht es hier.