27
.
2
.
2018

Angular, RxJS und der Fluxkompensator

Wichtige Kniffe für Reactive Programming mit RxJS

EINLEITUNG

Der Weg von AngularJS (=Version 1) zu Angular (=Version 2+) war Anfangs kein leichter. Das Framework wartete mit einer neuen Form der asynchronen Programmierung names Reactive Programming und dem zugehörigen Framework RxJS auf. Gerade noch warf man fröhlich mit Callbacks und Promises um sich, da war das schon wieder ein alter Hut - Observables und Subjects sind nun en vogue. Mittlerweile fällt mir der Umgang damit leicht, doch zu Anfangs war es schwer. Daher widmet sich dieser BlogPost einigen Anwendungsfällen von RxJS innerhalb einer Angular Anwendung, welche dem WebWorker in seiner täglichen Arbeit des Öfteren begegnen. Dabei ist es wichtig zu verstehen, dass alle Abläufe zeitabhängig und nebenläufig sind. So passieren gewisse Dinge vor anderen Dingen obwohl man es so nicht erwartet. Ein Gespür für die Zeit, Zukunft und Vergangenheit ist dabei entscheidend … Doch was zum Geier ist ein Gigawatt?

 

GRUNDLAGEN VON RXJS

Das Thema ist sehr kopmplex und wer noch nie damit gearbeitet hat, dem wird das Folgende auch nicht groß weiterhelfen, daher am besten in aller Ruhe zuerst ein Buch wie RxJS in Action lesen. Nichtsdestotrotz hier die Mikrozusammenfassung von RxJS:

  • Es gibt immer einen Sender und einen/mehrere Empfänger.
  • Der Sender sendet eine oder mehrere Nachrichten innerhalb eines Zeitraumes.
  • Ein Empfänger (Subscriber) kann sich beim Sender mittels subscribe() zum Empfang von Nachrichten registrieren.
  • Der Empfänger erhält beim subscriben eine Anmeldung (Subscription) mit derer er sich durch Subscription.unsubscribe() vom Empfang neuer Nachrichten abmelden kann.
  • Ein Sender (Subject) sendet mittels next(Wert) eine Nachricht an alle registrierten Empfänger.
  • Damit ein Empfänger nicht selbst einen Sender “missbrauchen” und einfach Subject.next(bla) senden kann, gibt man i.d.R. das Subject nicht an den Empfänger heraus sondern nutzt Subject.asObservable() (Kapselung).
  • Ein Observable<Typ> ist im Prinzip ein Subject dessen API nur Empfang (subscribe) anbietet.
  • Hat ein Sender seine letzte Nachricht gesendet, sendet er i.d.R Subject.complete().
  • Observables lassen sich mittels Operatoren wie take()takeUntil() oder flatMap() kombinieren.

Das waren jetzt zumindest alle Begrifflichkeiten und wir werden später in den Anwendungsfällen auf Besonderheiten eingehen. Es sei aber gesagt, dass folgende Aspekte von RxJS im Umgang damit entscheidend sind.

  • Zeitpunkt des Subscribens der Empfänger (Subscriber).
  • Zeitpunkt des Sendens der ersten Nachricht (Subject).
  • Anzahl von Nachrichten die vom Sender gesendet werden (Subject).
  • Anzahl von Nachrichten die man empfangen möchte (Subscriber).
  • Zeitpunkt der Abmeldung vom Empfang (Subscriber).
  • Gewünschtes Verhalten beim Subscriben, wenn bereits zuvor Nachrichten gesendet wurden.
Subscriber und Subject
‍Subscriber und Subject

Im obigen Schaubild loggt der Empfänger lediglich Bar. Denn der Empfänger hat zu einem Zeitpunkt subscribed, an dem bereits die erste Nachricht Foo gesendet wurde. Und der Empfänger hat sich auch abgemeldet (unsubscribe) bevor die letzte Nachricht Foobar gesendet wurde. Später lernen noch andere Typen von Subjects kennen, die sich anders verhalten als der eben beschriebene Fall.

Es ist immer das Wichtigste sich über diese Dinge im Klaren zu sein, sonst wird man Code schreiben, der nicht die Realität widerspiegelt und zu nicht deterministischen Ergebnissen führt.

 

ANGULAR KOMPONENTEN LEBENSZYKLUS

Nachdem wir nun wissen, wie RxJS prinzipiell funktioniert, müssen wir auch verstehen wie der Angular Komponenten Lebenszyklus funktioniert. Viele kennen den ‘Konstruktor’ aus anderen Programmiersprachen wie Java. Doch so wie auch Java mit JSR-250 gewisse Lifecycle Hooks wie @PostConstruct() und @PreDestroy() anbietet, so implementiert Angular ein ähnliches Konzept für seine Komponenten.

  • TIPP Ein Angular Service kennt keine Lifecycle Hooks. Er hat nur einen Konstuktor.

Wichtig zu wissen ist, dass nur @Component() und @Directive() diese Lifecycle Hooks anbieten. Services aka @Injectable() haben keine Lifefcycle Hooks.

  • TIPP Der Konstruktor wird i.d.R. nur zur Dependency Injection genutzt.

Wie in anderen Programmiersprachen nutzen wir den Konstruktor lediglich zur Dependency Injection, wir laden keine Daten und versuchen den Konstruktor klein und performant zu halten.

Hier mal vereinfacht die wichtigsten Lifecycle Hooks dargestellt.

vereinfachter Angular Komponenten Lebenszyklus
‍vereinfachter Angular Komponenten Lebenszyklus

Wollen wir also Daten innerhalb einer Komponente nur einmalig laden, so bietet sich ngOnInit() an. Verändert sich unsere Komponente durch Änderungen von Außen (@Input()) sollten wir das mit ngOnChanges() behandeln. Und haben wir offene Subscribtions, dann sollten wir diese in der ngOnDestroy() unsubscriben.

Nachdem wir nun viel Theorie behandelt haben, kommen wir nun zum praktischen Teil und schauen uns im Folgenden einige Praxisbeispiele an.

 

FALL 1: ZEITVERSATZ BEIM LADEN VON INITIALEN DATEN

Dieser Fall begegnet einem früher oder später in jedem Projekt. Nehmen wir an unsere Anwendung hat eine Config die beim Initialisieren der Angular Anwendung vom Backend geladen werden soll und künftig im ApiService zur Verfügung stehen soll.

Der naive und fehleranfällige Ansatz dies zu bewerkstelligen stellt sich wie folgt dar.

'naiver' Aufbau Fall 1
‍'naiver' Aufbau Fall 1

Das Problem ist, dass die Config unter Umständen zu spät geladen wird und beim Aufruf von this.config.pageSize im Service die Variable this.config den Wert undefined hat. Man erhält dann einen Fehler in der Form von “pageSize is not a property of undefined”. Die Problematik des zeitlichen Versatzes lässt sich wie folgt darstellen.

Zeitversatz Problematik
‍Zeitversatz Problematik

 

ZEITPUNKT DES SUBSCRIBENS UND DAS VERHALTEN VON RXJS SUBJECTS

Wir haben bereits einleitend den “Zeitpunkt des Subscribens” als wichtigen Faktor beschrieben. In der Regel erhält ein Subscriber nur die Nachrichten, die gesendet werden während er subscribed ist. Dies gilt wenn man die Klasse Subject verwendet. Will man dass der Empfänger auch die Nachrichten empfängt, die gesendet wurden, bevor er sich subscribed hat, so sollte man die Klassen BehaviorSubject und ReplaySubject nutzen. Wir erklären deren Verhalten kurz anhand folgender Darstellungen.

ReplaySubject mit zwei Subscribern
‍ReplaySubject mit zwei Subscribern

Das ReplaySubject hat keinen Initialwert, spielt aber alle zuvor gesendeten Nachrichten an neue Subscriber ab.

BehaviorSubject mit zwei Subscribern
‍BehaviorSubject mit zwei Subscribern

Das BehaviorSubject hat einen Initialwert und spielt nur die zuletzt gesendete Nachricht an neuer Subscriber ab.

Dieses Verhalten kann man sich auf vielfältige Weise zu Nutze machen. Im Folgenden stellen wir ein paar Praxisbespiele vor.

 

ZURÜCK ZU FALL 1: REPLAYSUBJECT UND FLATMAP TO THE RESCUE

Nach diesem Exkurs zur Thematik des Subscribe-Zeitpunktes und den verschiedenen Subject-Typen, sollten wir nun in der Lage sein den für uns passenden auszuwählen.

In unserem Fall müssen wir einen Weg finden die config initial im Konstruktor des ApiService zu laden und die loadUser() Methode darf erst Daten liefern, wenn das geschehen ist. Jeder neue Aufruf von loadUser() durch eine andere Komponente zu einem anderen Zeitpunkt muss dabei auch funktionieren.

Wir entscheiden uns für das ReplaySubject und den flatMap/mergeMap Operator. Der Quellcode des ApiService ändert sich wie folgt:

github:3574cf20be2d9f41e2b695587b119ce6

Gut an dieser Methode ist, dass auch künftige Subscriber bedient werden. Subscribed ein neuer Subscriber, dann spielt das ReplaySubject das letzte true (aus dem Konstruktor) ab und die loadUsers Methode liefert uns die User vom Backend. Das Laden der Config geschieht somit wirklich nur einmalig im Konstruktor.

 

FALL 2: UNSUBSCRIBEN IM DEKONSTRUKTOR MIT TAKEUNTIL OPERATOR

Wir haben bisher nur das Subscriben des Empfängers beim Sender behandelt. Doch was ist, mit dem Unsubscriben? Eine gute Frage. In der Angular Doku taucht an mehreren Stellen auf ‘Use ngOnDestroy() to unsubscribe Subscriptions’. Das kann man auf naive und elegante Weise tun. Nehmen wir an, wir haben eine Komponente, welche auf ScrollEvents reagiert und auch die neusten Aktienkurse per WebSocket gepushed bekommt. Kurz gesagt, die Komponente hört auf zwei Sender die über die Zeit mehrfach Nachrichten senden.

Der naive Ansatz sieht so aus, dass wir die Subscriptions explizit unsubscriben.

github:990be55703104c872e502887b2afc919

 

Der elegante Ansatz nutzt den takeUntil Operator, um implizit zu unsubscriben.

github:67cc97d0a0b5c3d038540dd21076f55b

 

Wie wir anhand des Beispiels sehen können ist der elegante Ansatz lesbarer und reduziert Boilerplate-Code. Ich persönlich fordere es in meinen Projekten ein, dass jedes subscribe() mit takeUntil oder take geprefixed wird, dadurch sehen andere Entwickler, was die Intention des Entwicklers war und welches Verhalten er sich vom Subject erwartet hat.

 

FALL 3: EXPLIZITE VERWENDUNG VON TAKE(ANZAHL)

Wie bereits in ‘Fall 2’ beschrieben finde ich den Ansatz mit dem takeUntil Operator sehr elegant. Doch auch dort, wo man weiß, dass man nur an der ersten Nachricht des Sender interessiert ist und man eigentlich weiß, dass der Sender nur einmalig sendet und sich dann completed, ist es wichtig explizit einen Operator zu verwenden, damit der Code lesbarer wird.

Nehmen wir beispielsweise den Angular HttpClient, welcher bei einem GET Request nur einmal die Response sendet und dann completed. Doch kapseln wir den HttpClient in einem Service ist das unter Umständen nicht mehr direkt ersichtlich. Daher rate ich zur Verwendung von expliziten take(Anzahl) Operatoren. Im folgenden Beispiel geben wir explizit an, dass wir nur an einer Nachricht interessiert sind und sich die Subscription danach direkt unsubscriben soll.

github:53be2ddd14e251669aaaa9b6940d0fc0

Mit take oder takeUntil vermeiden wir Zombie-Subscriptions die zu nicht deterministischem Verhalten und Fehlern in unserer Anwendung führen. Es empfiehlt sich immer darüber nachzudenken, wann man eine Subscription nicht mehr benötigt und diese so früh wie möglich zu unsubscriben.

 

FAZIT

Wir haben gezeigt, was SubjectsSubscriptions und Observables sind. Wie sie vielfältig eingesetzt und mit Operatoren verknüpft werden können und wie man dieses Wissen richtig in Kombination mit dem Angular Komponenten Lebenszyklus nutzt. Interessant sind auch Projekte wie cartant/rxjs-marbles die das Testen von solchen Zeitversatz-Problemen erleichtern. Auch hilft es die interaktiven Diagramme auf rxmarbles.com zu nutzen, um sich Operatoren verständlich zu machen. Aber vergesst nicht die benötigten 1.12 GigaWatt für den Fluxkompensator!

Bernhard
Software Engineer - Java, JVM

Themen