Angular, RxJS und der Fluxkompensator

Reactive Programming mit RxJS

Bernhard @ Comsysto Reply
comsystoreply

--

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 komplex 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 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 () sendet mittels 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 , oder 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.

Im obigen Schaubild loggt der Empfänger lediglich . Denn der Empfänger hat zu einem Zeitpunkt subscribed, an dem bereits die erste Nachricht gesendet wurde. Und der Empfänger hat sich auch abgemeldet (unsubscribe) bevor die letzte Nachricht 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.

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

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

Wollen wir also Daten innerhalb einer Komponente nur einmalig laden, so bietet sich 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

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

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

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

‍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 Operator. Der Quellcode des ApiService ändert sich wie folgt:

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.

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

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.

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 Subjects, Subscriptions 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!

This blogpost is published by Comsysto Reply GmbH

--

--