JAX Blog

Die Untiefen reaktiver
Program­mierung

Reactive Programming ist die Zukunft. Aber ist es auch einfach?

Feb 22, 2022

Vor acht Jahren wurde das Reactive Manifesto [1] veröffentlicht, und ganz allmählich beginnt die reaktive Programmierung auch in der Java-Welt Fuß zu fassen. Deswegen lohnt es sich, die Erfahrungen und Best Practices aus einer anderen Community anzuschauen. Angular hat von Anfang an sehr stark auf RxJS gesetzt und sich damit eine ganze Menge Komplexität eingehandelt. Wie kann man sie beherrschbar machen und was können Java-Entwickler daraus lernen?

Haben Sie sich schon einmal gefragt, warum Quarkus oder Netty so schnell sind? Bei Quarkus könnte die Antwort GraalVM lauten, und das wäre sogar richtig. Aber wie sieht es mit Netty aus? Oder Vert.x? Oder warum fühlt sich ein modernes UI mit Angular oder React.js flotter an als die meisten JSF- oder Spring-MVC-Anwendungen? Die Gemeinsamkeit ist die non-blocking IO. Dahinter steckt die Erkenntnis, dass unser Computer die meiste Zeit mit Warten verbringt. Unsere modernen Gigahertz-CPUs können ihr Potenzial gar nicht ausspielen. Kurze Sprints werden abgelöst von schier endlosen Wartezeiten. Irgendwo ist immer ein Stau. Jeder Datenbankzugriff bedeutet mehrere Millisekunden Pause, jeder Zugriff auf ein REST-Backend dauert oft 10 bis 100 Millisekunden, und das summiert sich schnell auf mehrere Sekunden. Die Idee der reaktiven Programmierung ist, die Wartezeiten sinnvoll zu nutzen. Dafür zerschneidet man einen Algorithmus in zwei Teile. Alles, was nach dem REST-Call kommt, wird in eine eigene Funktion ausgelagert. Euer Framework ruft diese Callback-Funktion auf, wenn die Daten angekommen sind. In der Zwischenzeit kann die CPU andere Aufgaben erledigen. Sie kann zum Beispiel einfach mit der nächsten Zeile weitermachen. Beim traditionellen, blockierenden Programmiermodell definiert die nächste Zeile, was wir mit dem Ergebnis der REST-Calls machen wollen. Aber das ist jetzt nicht mehr der Fall. Dieser Teil des Algorithmus wurde ja in die Callback-Funktion verschoben. Es gibt also keinen Grund mehr, mit der Abarbeitung der nächsten Zeile zu warten. Damit können sehr viele Performanceprobleme gelöst werden. Solche Callback-Funktionen sind erst seit Java 8 sinnvoll möglich, als die Lambdafunktionen eingeführt wurden. Vorher konnte man sie mit anonymen inneren Klassen simulieren, aber das war so unattraktiv, dass es kaum jemand gemacht hat. Dadurch erklärt sich, warum reaktive Frameworks wie Quarkus oder Spring Reactor erst seit relativ kurzer Zeit populär werden. Die JavaScript-Welt hat hier einige Jahre Vorsprung, und deswegen soll jetzt RxJS betrachtet werden.

Nichtlineare Programmierung mit RxJS

Auf den nächsten Seiten bewegen wir uns ausschließlich im JavaScript Frontend. Das hat einige Konsequenzen. JavaScript kennt kein Multi-Threading. Das macht aber nichts, es muss ja nur noch ein einziger Anwender bedient werden. Wir reden der Einfachheit halber jetzt auch nur noch über REST-Calls.

Es wird also ein REST-Call zum Backend geschickt. Die Idee ist, die Wartezeit, bis die Antwort eintrudelt, zu nutzen. Das bedeutet, dass sofort mit der nächsten Zeile im Programmcode weitergemacht werden kann. Erfahrene RxJS-Entwickler werden jetzt mit den Achseln zucken und „Ja, und?“ sagen. Für Neueinsteiger ist das aber ein erhebliches Problem. Die Reihenfolge, in der der Programmcode ausgeführt wird, ist nicht mehr linear. In vielen Fällen ist er auch nicht mehr deterministisch. Schauen wir uns ein einfaches Beispiel an (Listing 1).

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Alles neu?

Wir sind alle Entwickler und Technologieenthusiasten und daher kommen wir sehr schnell zu der Überzeugung, dass mit Framework X oder Technologie Y alle Probleme schnell gelöst werden und damit auch ganz neuen Herausforderungen und Anforderungen leicht entsprochen werden kann. Einige wollen auch dafür eigene Technologien entwickeln, was gar nicht mal ungewöhnlich ist – man liest und hört nur sehr wenig von diesen Ansätzen. Die meisten Seniorentwickler schreiben die gleiche Art von Anwendung oder lösen die gleiche Art von Problemen Tag für Tag, Jahr um Jahr. Das ist manchmal langweilig, und was gibt es Spannenderes, als ein eigenes Framework zu schreiben? Vielleicht sogar mit der Absicht, es danach als Open-Source-Software zu veröffentlichen? Die Antwort ist: Wahrscheinlich ist es superspannend, aber bringt es unsere Anwendung wirklich weiter? Und viel wichtiger: Lösen wir damit überhaupt die aktuellen Probleme?

Um diese Fragen zu beantworten, müssen die Anwendung und ihre Architektur erst einmal überprüft werden. Bei einer solchen Architekturreview, die idealerweise von Externen durchgeführt wird, hat sich in der Praxis ATAM [1] als Methode bewährt. Dieser Artikel ist zu kurz, um auf ATAM einzugehen, aber ganz kompakt formuliert, definiert man via Szenarien Qualitätsanforderungen an den Sollzustand der Architektur und vergleicht diesen dann mit dem Istzustand des Systems. Das klingt erst einmal furchtbar aufwendig und teuer, daher wird die Investition oft gescheut. Jedoch stehen die Kosten aus unserer Erfahrung in keinem Verhältnis zu den Kosten (über den Lifecycle) einer nicht passenden Architektur oder Technologiewahl. Ähnlich wie bei Fahrzeugen sollte daher ein regelmäßiger TÜV der Architektur zu einer reifen Produktentwicklung dazugehören.

console.log(1);
httpClient.get<BOOK>('https://example.com/books/123').subscribe(
  (book) => console.log(`2 ${book}`));
httpClient.get<BOOK>('https://example.com/books/234').subscribe(
  (book) => console.log(`3 ${book}`);
console.log(4);

Dieser Quelltext schickt zwei GET Requests an einen fiktiven REST-Server ab, um Informationen über zwei Bücher zu bekommen. Sie werden kurzerhand auf der Entwicklerkonsole ausgegeben, zusammen mit einer durchnummerierten Zahl. Wir drucken also zunächst die Zahl 1, rufen dann das erste Buch ab, dann das zweite, und zum Schluss noch die Zahl 4.

In welcher Reihenfolge werden die Konsolenausgaben erscheinen? Die naive Antwort wäre 1, 2, 3, 4. Tatsächlich ist dieser Programmcode aber nichtdeterministisch. Es kommt darauf an, welcher REST-Call zuerst eine Antwort liefert. Es ist nur klar, dass die Zahl 4 vor der 2 und der 3 kommt.

Das ist auch gut so. Genau das soll erreicht werden. Die Informationen über beide Bücher werden gleichzeitig abgefragt, und als Sahnehäubchen ist die Anwendung auch während des Server-Requests bedienbar. In welcher Reihenfolge der Server die Requests beantwortet, ist nicht klar. Die Requests laufen eben wirklich parallel. Und es kommt noch besser: Egal ob der REST-Call 50 Millisekunden oder 50 Sekunden braucht, die Anwendung ist in der Zwischenzeit für weitere Benutzeraktivitäten verfügbar. Geschickt eingesetzt, fühlt sich die Anwendung dadurch sehr viel flüssiger an als beim traditionellen Ansatz, bei dem die Anwendung während der gesamten Wartezeit einfriert. Reactive Programming ist der natürliche Feind des Wait Cursors.

Nichtsdestotrotz gibt es einen gravierenden Nachteil. Das Programm wird nicht mehr Zeile für Zeile abgearbeitet. Stattdessen gibt es einen nichtlinearen Programmfluss. Die Callbacks stehen normalerweise oberhalb der Zeile, die zuerst abgearbeitet werden. Für Neueinsteiger kann das eine nicht zu unterschätzende Hürde sein. An den nichtlinearen Programmfluss muss man sich erst einmal gewöhnen. Und auch wenn die alten Hasen das selten zugeben: Wenn es komplizierter wird, verliert jeder von uns den Überblick. Manche früher, manche später. Wenn man beruflich programmiert, ist das ein wichtiger Punkt. Es gibt ein Mantra, das viele auswendig mitsprechen können: „Es ist nicht so wichtig, dass ihr euren eigenen Quelltext versteht. Es kommt darauf an, dass jeder eurer Kollegen euren Programmcode versteht“. Der Abstraktionslevel, der im aktuellen Team genau richtig ist, kann das nächste Team überfordern. Man muss sich also ständig nach den jeweiligen Teammitgliedern richten, und sie entweder coachen oder das eigene Abstraktionslevel an das Team anpassen.

Die Kunst des Beobachtens

Gehen wir einen Schritt weiter. Als Nächstes wollen wir erst die Liste aller Bücher holen und dann die Einzelheiten für jedes einzelne Buch. Das Prinzip wurde hier schon beschrieben. Es gibt eine subscribe()-Methode, die aufgerufen wird, wenn der Server Daten liefert. Was wäre also einfacher, als den Algorithmus wie in Listing 2 zu formulieren?

httpClient.get<Array<number>>('https://example.com/books')
    .subscribe((isbns) => {
  ids.forEach((isbns: number) => {
    httpClient.get<Book>(`https://example.com/books/${ isbns }`)
      .subscribe((book) => console.log(`${book}`));
  });
});

Das funktioniert, sollte aber besser so nicht umgesetzt werden. Ich habe bei diesem Minibeispiel mehrere Minuten damit verbracht, die Klammern richtig zu setzen. Und reale Projekte sind komplizierter, da ist die Klammersetzung das kleinste Problem. Es sollte also ein einfacher Quelltext geschrieben werden. RxJS bietet dafür ein reiches Repertoire an Operatoren. Meistens muss man überhaupt keinen Quelltext in die subscribe()-Methode schreiben. Ein Aufruf ohne Callback-Funktion reicht. Um die Idee zu illustrieren, beschränken wir uns zunächst auf den REST-Call, der die Liste der ISBNs (oder IDs) der Bücher liefert (Listing 3).

const promisedIsbns: Observable<Array<number>> =
  httpClient.get<Array<number>>('https://example.com/books');
 
promisedIsbns.pipe(
  tap((isbns: Array<number>) => console.log(isbns))
)
promisedIsbns.subscribe();

Listing 3 macht deutlich, dass http.get() ein Observable zurückliefert. Das ist eine sehr schöne Metapher: Man schickt den REST-Call los, und danach wird beobachtet, was der Server macht. Während des Beobachtens hat man Zeit für andere Dinge. Das ist ähnlich wie im wirklichen Leben, etwa beim Bügeln während des Fernsehens. Wenn es im Fernsehen spannend wird, wird das Bügeleisen kurz zur Seite gelegt, und während der Werbepause macht man mit dem Bügeln weiter. Als Nächstes definieren wir eine Pipeline. Auch das ist eine sehr schöne Metapher. Wenn man reaktiv programmiert, sollte man aufhören, Daten als etwas Statisches zu betrachten. Die Daten müssen als Datenstrom betrachtet werden. Wird die Sache weitergedacht, sind die Daten in der Datenbank so gut wie nie interessant. Sie werden immer nur dann interessant, wenn sie sich verändern. Dieser Gedanke führt dann zu Systemen, wie z. B. Apache Kafka. Das ist ein sehr mächtiger und fruchtbringender Paradigmenwechsel. Aber ich greife vor – noch sind wir ja im Frontend unterwegs.

Die Pipeline besteht aus einer Reihe von Operatoren, die der Reihe nach ausgeführt werden. In unserem Fall ist es der Operator tap, der auch Seiteneffektoperator genannt wird. Er lässt den Datenstrom unverändert passieren, erlaubt es aber, mit den Daten zu arbeiten. In diesem Fall wird die Liste der ISBNs einfach auf der Konsole ausgegeben. Als letztes Kommando kommt noch der Aufruf von subscribe(). RxJS evaluiert Ausdrücke erst dann, wenn sie gebraucht werden – das heißt, wenn sie jemand abonniert. Ohne den Aufruf von subscribe() startet der Algorithmus also nicht.

 

Wenn Beobachter Beobachter beobachten

Zurück zu unserer ursprünglichen Aufgabe. Es sollten die Information über alle Bücher gesammelt werden. Wir können also eine Pipeline mit den Operatoren map() und tap() aufbauen (Listing 4).

const ids: Observable<Array<number>> = httpClient.get<Array<number>>('https://example.com/books');
ids.pipe(
  map(
    (isbns: Array<number>) => ibns.map((isbn) => 
       httpClient.get<Book>(`https://example.com/books/${isbn}`)),
  tap((books: Array<Observable<Book>>) => console.log('Hilfe!'))
  )
);
ids.subscribe();

Tja, das hatten wir uns einfacher vorgestellt. Jetzt gibt es zwei Probleme. Zum einen enthält unsere Pipeline ein Array von ISBNs. Die Pipeline funktioniert aber am besten, wenn das Array in einen Strom von Einzelwerten aufgelöst wird. Das wird am doppelten map() deutlich. Und zum anderen liefert der verschachtelte Aufruf von http.get() ein Observable. Das Interessante ist aber der elementare Datentyp. Die Liste der Bücher wird benötigt, denn mit den Beobachtern der Bücher lässt sich nichts anfangen. Keine Panik, das Internet ist voll mit Ratschlägen, wie das Problem zu lösen ist. Und alle preisen euphorisch, dass die Lösung sehr einfach ist: „Du musst nur den forkJoin()-Operator und den flatMap()-Operator verwenden“. Das stimmt, weckt bei mir aber Zweifel, ob das hier der richtige Weg ist. RxJS hat eine sehr große Zahl von Operatoren – und meine Theorie ist, dass die meisten Operatoren Probleme lösen, die es ohne RxJS nicht gäbe. Die Umstellung auf die reaktive Programmierung stellt sich als verblüffend vertrackt heraus. Wohlgemerkt: Ich bin ein sehr großer Fan von RxJS. Es bietet ein sehr elegantes API für viele Anwendungsfälle. Ich bezweifele aber, dass Observables die richtigen Metaphern für REST-Calls sind. Ein Observable erlaubt es, einen potenziell unendlichen Strom von Daten zu beobachten. Observables sind die perfekte Lösung für WebSockets, Tastatureingaben oder auch für einen Apache-Kafka-Stream. Bei REST-Calls kommt die Metapher an ihre Grenzen. Ein REST-Call liefert maximal ein Ergebnis.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Ein Internet voller Versprechungen

Den Entwicklern von RxJS ist das auch klar, und sie bieten eine passende Lösung dafür an. Sie hat, völlig zu Unrecht, einen schlechten Ruf in der Angular-Welt. Ihr könnt ein Observable in ein Promise umwandeln. Und wenn man das mit den JavaScript-Schlüsselwörtern async und await verknüpft, wird der Quelltext fast immer dramatisch einfacher (Listing 5).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const books: Array<Book> = await isbns.map(
  async (isbn) => await firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
books.forEach((book) => console.log(book));

Unser Ziel ist erreicht. Es ist wieder ein linearer Quelltext entstanden, der leicht verständlich ist. Aber da fehlt doch noch etwas. In der map()-Methode steht das Schlüsselwort await. Im Endeffekt gibt es eine for-Schleife, die den Programmfluss blockiert. Aber wir wollten doch eine non-blocking IO haben.

Blockadebrecher

Zum Glück legt JavaScript ohnehin sein Veto ein und bricht mit einer Fehlermeldung ab. Das await vor dem map() kann nicht funktionieren, weil map() kein einzelnes Promise, sondern gleich ein ganzes Array von Promises liefert (Listing 6). Also rufen wir die Funktion Promise.all() zu Hilfe.

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const promisedBooks: Array<Promise<Book>> = 
  isbns.map((isbn) => firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
const books = await Promise.allSettled(promisedBooks);
books.forEach((book) => console.log(book));

Retries, Timeouts und take(1)

In meinem Projekt verwende ich diesen Ansatz sehr erfolgreich und mit großer Begeisterung. Umso erstaunlicher, dass der Rest der Angular-Welt die Möglichkeit, Observables in Promises zu verwandelt, ignoriert oder sogar leidenschaftlich bekämpft. Ein beliebtes Gegenargument ist, dass async/await den Vorteil der reaktiven Programmierung verspielt, indem es alles künstlich wieder linearisiert. Die Gefahr besteht, doch man kann, wie gerade gesehen, leicht gegensteuern. Hierfür packen wir einfach Promise.allSettled() in den Werkzeugkoffer. Auf der Suche nach Gegenargumenten bin ich noch darauf gestoßen, dass firstValueFrom() weder Retries noch Timeouts unterstützt. Das ist aber ein Scheinargument. Auch wenn sie in Promises verwandelt werden, man hat es immer noch mit Observables zu tun. Damit haben wir die gesamte Power von RxJS zur Verfügung (Listing 7).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books')
    .pipe(
      timeout(1000), 
      retry(5))
);

Wem das zu umständlich ist, der kann einfach eine Funktion definieren, die sowohl firstValueFrom() als auch Operatoren timeout() und retry() aufruft. Etwas unangenehmer ist eine Eigenschaft der Methode toPromise(), die in der älteren Version RxJS 6 anstelle von firstValueFrom() verwendet werden muss. toPromise() feuert erst, wenn das Observable das complete Event schickt. Bei REST-Calls ist das kein Problem, aber wenn man das Ergebnis in einem Subject zwischenspeichert, wird das complete Event niemals geschickt. Dieses Pattern ist umständlich, löst aber das Problem:

const isbns: Array<number> = 
      await books$.pipe(take(1)).toPromise();

 

Wenn das alles so einfach ist – warum verwendet es nicht jeder?

Jetzt wird es philosophisch. Observables erzwingen einen funktionalen Programmierstil, und das wiederum hat eine magische Anziehungskraft auf viele Entwickler. Funktional geschriebene Algorithmen bestehen im besten Fall aus einer langen Kette kurzer Zeilen. Jede Zeile kümmert sich um genau eine Aufgabe. Das fühlt sich einfach gut an und hinterlässt den Eindruck, den Algorithmus optimal strukturiert zu haben. Dieser Eindruck verfliegt schnell, wenn man denselben Algorithmus prozedural neu schreibt und ihn danebenlegt. Man könnte den prozeduralen Algorithmus genauso gut in viele kleine Einzeiler aufteilen. Oft genug finde ich den Quelltext wesentlich übersichtlicher. An dieser Stelle wird die Diskussion meistens sehr emotional. Viele Entwickler finden den prozeduralen Programmierstil unübersichtlich und schwer verständlich. Vielleicht macht es sich bemerkbar, dass ich den funktionalen Programmierstil erst sehr spät kennengelernt habe. Die Idee bei JavaScript – und auch bei Java – ist, einfach beides anzubieten. Es kann der gute alte prozedurale Programmierstil verwendet werden, wenn es passt, und wenn das funktionale Paradigma besser passt, verwendet man eben das. Es ist sogar so, dass meine TypeScript- und JavaScript-Programme deutlich mehr funktionale Anteile enthalten als meine Java-Programme, weil ich das funktionale API von JavaScript für wesentlich eleganter halte. Mein Verdacht ist, dass es einfach eine Frage der Übung, der Mode und des Geschmacks ist. Bei meinen Vorträgen, in denen ich das async/await-Pattern propagiere, bekomme ich fast immer Gegenwind. Wenn ich dann zurückfrage, woher der Widerstand kommt, höre ich selten substanzielle Argumente. Diese Argumente gibt es allerdings durchaus. Lest euch nur mal das leidenschaftliche und gut begründete Plädoyer gegen async/await von Daniel Caldas [2] durch. Das sind aber selten die Argumente, die mir spontan genannt werden. Ich befürchte, das Hauptproblem ist, dass alle bei ihrer Einarbeitung in Angular verinnerlicht haben, dass RxJS und Observables das Mittel der Wahl und ein großer Fortschritt sind. Was auch richtig ist, nur setzt sich dadurch im Unterbewusstsein die Überzeugung fest, dass async/await schlecht ist. Der Witz ist nur, dass sich das Angular-Team zu einem denkbar ungünstigen Zeitpunkt für Observables entschieden hat. AngularJS 1.x hatte noch Promises verwendet, allerdings mit der alten umständlichen Syntax. Wenn man diesen Programmierstil mit dem Programmierstil von RxJS vergleicht, ist beides in etwa gleich unhandlich, aber RxJS bietet sehr viel mehr Möglichkeiten als die native Verwendung von Promises. Die Abkehr von Promises war folgerichtig. Rund ein Jahr später wurde das neue Schlüsselwortpaar async/await in JavaScript eingeführt. Ob sich das Angular-Team für RxJS entschieden hätte, wenn async/await schon Mainstream gewesen wäre?

Wie sieht das in Java aus? Dieser Artikel basiert auf einem Vortrag für Angular-Entwickler. Als ich den Vortrag dem Java Magazin angeboten hatte, dachte ich mir, dass es interessant sein könnte, die Erkenntnisse auf Java zu übertragen. Leider war das nicht so fruchtbringend wie erhofft. In erster Linie habe ich beim Recherchieren festgestellt, dass Java eben anders tickt als JavaScript.

Fangen wir mit dem Offensichtlichen an. In Java gibt es kein async/await. Stattdessen gibt es in den Java-Frameworks, die ich mir angeschaut habe, den Operator block(), der aus einem Mono wieder den eigentlichen Wert machen kann. Analog gibt es bei Java den Operator collectList() für ein Flux. Was sich hinter den Begriffen Mono und Flux verbirgt, erzähle ich euch gleich. Die Operatoren block()und collectList() blockieren den Programmfluss. Das klingt schlimmer als es ist: Im Gegensatz zu JavaScript versteht sich Java auf die Kunst des Multi-Threadings. Man blockiert sich also nur selbst, aber nicht gleich den ganzen Server. Es könnte auch sein, dass das Framework gezwungen ist, von einer optimalen Multi-Threading-Strategie auf eine weniger effiziente Strategie umzuschalten. Aber das ist auch alles. Das Rezept, das ich oben vorgestellt habe, funktioniert in Java also nicht. Man muss sich auf die funktionale Programmierung einstellen. Übertreiben sollte man es aber nicht. Faustregel: Wenn man ein if-Statement braucht, extrahiert man den Code einfach in eine Methode. Ich hatte schon kurz Flux und Mono (oder Uni in Quarkus) angesprochen. Das ist ein interessanter Unterschied zu RxJS. Quarkus und Spring Reactive unterscheiden zwischen Datenströmen, die viele Ergebnisse liefern (Flux) und einfachen Requests, die nur ein Ergebnis liefern (Mono bzw. Uni). Als ich das gelesen habe, fand ich das verblüffend. In der Angular-Welt gibt es eine lebhafte Diskussion, ob Promises oder Observables besser sind, und gleichzeitig unterstützen die Java-Frameworks kurzerhand beides gleichzeitig. Uni bzw. Mono entsprechen in etwa dem Promise und Flux entspricht dem Observable.

 

Zustände

Möglicherweise liegt der Hauptunterschied zwischen der reaktiven Programmierung mit Angular und mit Java ganz woanders. Heutzutage entwickeln die meisten Java-Entwickler REST-Services. Diese sind stateless. Alles ist im Fluss. Man muss sich nicht um statische Daten kümmern. Der Datenstrom, den die reaktiven APIs liefern, reicht vollkommen. In Angular hingegen dreht sich am Ende des Tages alles um den State der Anwendung. Das ist ein Aspekt, den ich in diesem Artikel noch nicht gezeigt hatte und der Angular-Entwickler – und vor allem die Neueinsteiger unter ihnen – immer wieder vor Probleme stellt. Es gehört zu den Best Practices der Angular-Welt, möglichst den kompletten State der Anwendung in Observables zu speichern. So ein Observable ist jedoch nur dafür gedacht, Änderungen des Zustands zu kommunizieren, nicht aber, den aktuellen Zustand zu speichern. Wenn man den aktuellen Zustand braucht, man erst einmal schauen, wo man ihn herbekommt. Für Java-Entwickler entfällt diese Notwendigkeit von vorneherein. Das macht das Leben für die Java-Entwickler leichter. Sie müssen viel seltener zwischen dem reaktiven und dem synchronen Code wechseln. Als Gemeinsamkeit gibt es natürlich die Notwendigkeit, Methoden wie flatMap() zu verwenden. Die Quarkus Sandbox [3] von Hantsy Bai enthält ein paar Beispiele dafür. Und was ist mit Annotationen? Ein komplett anderer Weg, reaktive Programmierung zu vereinfachen, ist mir bei Apache Kafka aufgefallen. Schaut euch mal das Tutorial von Baeldung [4] an. Dort wird eine Annotation verwendet, um eine Methode aufzurufen, wenn der Server ein Ergebnis liefert (Listing 8).

@KafkaListener(topics = "topicName", groupId = "foo") 
public void listenGroupFoo(String message) {
   System.out.println("Received Message in group foo: " + message); 
}

Das ist das Reaktive Pattern konsequent zu Ende gedacht. Die Kafka-Topics bieten eine sehr lose Kopplung zwischen Nachrichtenquelle und Nachrichtenempfänger. Das funktioniert natürlich nicht immer. Im ursprünglichen Beispiel, dem Select-Statement oder dem REST-Call, ist das zu unpraktisch. Es würde auch ein falsches Signal aussenden. Eine Angular-Anwendung ist keineswegs lose mit dem Backend gekoppelt. Wenn das Backend nicht verfügbar ist, ist ein Angular-Frontend nutzlos.

Aber lassen wir das mal kurz außer Acht. Dann kann man sehen, dass Annotationen ein schönes Stilmittel sind, um Algorithmen reaktiv zu implementieren. Der Clou ist, dass es hier nicht um Observables, Monos oder Fluxes geht. Die Parameterliste und der Rückgabetyp der Funktion sind die reinen Datentypen. Das vereinfacht die Programmierung enorm. Das Framework übernimmt die komplette Abstraktion. Wir brauchen nicht zu wissen, dass ein @KafkaListener im Grunde genommen auch nichts anderes ist als ein Observable.

Klingt das verrückt? Dann schaut es euch noch einmal genau an. In Angular hättet ihr vermutlich eine Methode listenToKafka(topic, groupId), die ein Observable zurückliefert. Analog hättet ihr in Spring Reactive eine Methode listenToKafka(topic, groupId), die ein Flux zurückliefert. In beiden Fällen kann in der subscribe()-Methode definiert werden, was mit den Daten passieren würde. Und dieser Algorithmus wiederum ist exakt der Inhalt der Methode listenToFoo() aus Listing 7.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Resumée

Das war ein wilder Ritt durch die Untiefen der reaktiven Programmierung. Die Kernbotschaft, die ich vermitteln will, ist aber ziemlich einfach. Reactive Programming ist etwas, mit dem man sich beschäftigen sollte. Die Anwender werden es euch danken. Bessere Performance kommt immer gut an.

Das heißt aber nicht, dass ihr euch blindlings in das Abenteuer stürzen solltet. Oder, doch, das solltet ihr schon. Mut zur Lücke ist immer gut. Irgendwo muss man ja anfangen. Ihr werdet dann aber schnell feststellen, dass reaktive Programmierung ihre Tücken hat. An diesem Punkt angekommen, ist der richtige Zeitpunkt, diesen Artikel (noch einmal) zu lesen. Es gibt Strategien, reaktive Programmierung beherrschbar zu machen. Im Internet wird meistens empfohlen, sich mit Haut und Haaren darauf einzulassen. Das funktioniert für viele Teams ziemlich gut. Man kann aber auch versuchen, die Komplexität zu reduzieren. Bei Angular ist async-await das Mittel der Wahl. Bei Java gibt es diese Möglichkeit nicht. Sie wird auch nicht benötigt. Beim reaktiven Programmieren wird es meistens erst dann schwierig, wenn auf den aktuellen Zustand der Anwendung zugegriffen werden soll. Solange zustandslose REST Services entwickelt werden, habt ihr diese Anforderung nicht. Und falls doch, hat auch Java noch das eine oder andere As im Ärmel. Apache Kafka zeigt, wie mit Annotationen den Abstraktionsgrad der Algorithmen deutlich reduziert werden kann.

 

Links & Literatur

[1] https://www.reactivemanifesto.org

[2] https://goodguydaniel.com/blog/why-reactive-programming

[3] https://hantsy.github.io/quarkus-sandbox/reactive.html

[4] https://www.baeldung.com/spring-kafka

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management