Functional Reactive Programming (FRP): Mehr als nur Datenströme und Lambdas #2

In unserem vorherigen Blogpost ging es um die Grundlagen von Functional Reactive Programming (FRP). Im zweiten Teil widmen wir uns nun der Implementierung des Programmierparadigmas mit Java und Spring.

Implementierung der reaktiven Programmierung

Warum sollte es mich überhaupt interessieren?

Typische serverseitige Anwendungen werden in einem zwingenden Stil entwickelt, der auf nachfolgenden Aufrufen von Operationen basiert, die auf einem Aufrufstapel abgelegt werden. Die Hauptfunktion des Aufrufstapels besteht darin, den Aufrufer einer bestimmten Routine zu verfolgen, die aufgerufene Routine auszuführen, während der Aufrufer in dem Prozess blockiert wird, und die Steuerung mit einem Rückgabewert an den Aufrufer zurückzugeben.

Bei ereignisgesteuerten Anwendungen ist der Aufrufstapel nicht das Hauptanliegen. Das Hauptanliegen besteht darin, Ereignisse von Vorlagen auszulösen und Ereignisströme von Beobachtern zu überwachen. Der große Unterschied zwischen ereignisgesteuertem und imperativem Stil besteht darin, dass der Aufrufer einen Thread nicht blockiert und festhält, während er auf eine Antwort wartet.

Die Ereignisschleife selbst kann Single-Threaded sein, aber die Parallelität wird weiterhin erreicht, während aufgerufene Routinen ihre Arbeit erledigen (und möglicherweise die E / A selbst blockieren), während die (manchmal Single-) Threaded-Ereignisschleife eingehende Anforderungen verarbeiten kann.

Betrachten wir einen zeitaufwendigen Prozess wie die Suche in großen Datenmengen, bei dem die gesamte Antwort als einzelner Stapel zurückgegeben wird: Wie werden die Elemente ausgegeben, nachdem sie gefunden wurden? Im nachfolgenden Beispiel werden Datensätze in einer großen Sammlung in MongoDB gespeichert, sodass die Suche nach Elementen, die eine Phrase enthalten, einige Zeit in Anspruch nimmt. Um die Benutzererfahrung zu verbessern, werden die Datensätze nacheinander auf dem Bildschirm angezeigt, ohne auf den Abschluss des gesamten Vorgangs zu warten.

Um diesen Effekt zu erzielen, müssen Sie Server-Sent Events (SSE) verwenden. API-Code, der den Stream von Filmen erzeugt, ist wirklich einfach. Beachten Sie einfach den Antwortmedientyp TEXT_EVENT_STREAM

@GetMapping(value = "/search", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
   public Flux<Movie> search(@RequestParam String query) {
      return movieRepository.findByQuery(query);
}

Auf der Clientseite müssen Sie den Ereignisstrom verarbeiten, indem Sie eingehende Nachrichten abhören.

setupStream(query:string):void {
  const url = 'http://localhost:8080/search?query=' + query;
  const eventSource = new EventSource(url);
  
   eventSource.addEventListener('message', event => {
     const movie = JSON.parse(event.data)
     this.movies.push(movie); //push movie to array observed by React
   }, false);
  
  eventSource.addEventListener('error', event => {
    if (event.eventPhase === EventSource.CLOSED) {
      eventSource.close()
    }
  }, false);
}

Einfach und simpel.

Beachten Sie, dass vom Server gesendete Ereignisse nicht vom Internet Explorer unterstützt werden. Sie müssen Polyfill verwenden, um die Kompatibilität mit Ihrem bevorzugten Browser sicherzustellen.

Reaktive Programmierunterstützung

Reactive Programming wird von den meisten gängigen Sprachen unterstützt, die die ReactiveX-API wie Java, JavaScript, C #, Scala, Kotlin, Python oder Go implementieren.

Für Java gibt es zwei populärste reaktive Bibliotheken - RX Java 2 und Project Reactor, die sich sehr ähnlich sind. Wir verwenden im unserem Projekt Reactor als Backend, da es standardmäßig in Spring verwendet wird, aber es gibt keine Hindernisse RX Java 2 auch mit Spring zu verwenden. Für die Front-End-Anwendung verwenden wir RxJS.

Reaktor ist eine vollständig blockierungsfreie Grundlage mit einem effektiven Nachfragemanagement. Es interagiert direkt mit der Java 8-Funktions-API, Completable Future, Stream und Duration.

Reactor bietet zwei reaktive zusammensetzbare APIs: Flux und Mono, die Reactive Extensions ausführlich implementieren. Der Fluss (fließfähig in RX Java 2) repräsentiert eine asynchrone Sequenz von 0 bis N emittierten Elementen. Mono (Single in RX Java 2) repräsentiert höchstens ein ausgegebenes Objekt.

Wie bei anderen reaktiven Bibliotheken gibt es eine Menge nützlicher Operatoren, die Sie in der Mono- und Flux-Dokumentation finden, oder Sie können eine hervorragende Visualisierung von Stream-Operatoren mit Marblediagrammen anzeigen.

 
debounce.png
 

Legacy-Anwendungen

Der Traum ist es, eine brandneue Anwendung zu schreiben, ohne von älteren Datenbanken oder APIs abhängig zu sein. Wo es möglich ist, implementieren wir den Ansatz der reaktiven Programmierung von oben nach unten mit Reactive Mongo (Spring Data unterstützt MongoDB, Apache Cassandra und Redis) und WebClient für die Netzwerkkommunikation. Trotzdem müssen wir mit externen APIs kommunizieren.

Zum Glück gibt es auch dafür eine Lösung. Anstatt auf den Abschluss externer API-Aufrufe zu warten, stellen reaktive Bibliotheken eine Reihe von Wrappern bereit, mit denen Streams über blockierende Ereignisse erstellt werden können. Dies ist keine perfekte Lösung, da darauf gewartet werden muss, bis die Anforderungen abgeschlossen sind. Daher wird die Stream-Verarbeitung nicht zugelassen, aber der Thread des Aufrufers wird nicht blockiert, was zu einem geringeren Rechenaufwand führt.

Nachteile?

Im Allgemeinen habe ich das Gefühl, dass ich effizienteren und besser lesbaren Code mit reaktivem anstatt mit imperativem Ansatz schreibe, aber reaktives Programmieren kann auch einige Nachteile haben.

Erstens ist dieses Paradigma speicherintensiver, da die meiste Zeit unveränderliche Datenströme gespeichert werden müssen.

Der Entwickler muss sich einige Mühe geben, um die Denkweise aufgrund eines anderen Programmierparadigmas zu ändern, das auch nicht so ausführlich beschrieben wird wie das am häufigsten verwendete objektorientierte Programmieren.

Für mich ist der größte Nachteil, dass das Debuggen während der Entwicklung komplizierter ist, da Sie nicht nur Ihre Implementierung debuggen, sondern auch auf die korrekte Verwendung der Stream-API achten müssen. Darüber hinaus ist Ihre Implementierung mit der Stream-API umschlossen.

Fazit?

All dies “zerkratzt” die Oberfläche bei der Entwicklung reaktiver Anwendungen. Es scheint, dass reaktives Programmieren nicht nur ein Trend ist, sondern vielmehr das Paradigma für die moderne Softwareentwicklung, dass das Schreiben effizienter und lesbarer Software fördert und das Benutzererlebnis auf die nächste Ebene bringt.

Unabhängig von der Sprache oder dem Toolkit, das Sie auswählen, ist es die einzige Möglichkeit, die Erwartungen der Benutzer zu erfüllen, wenn Skalierbarkeit und Ausfallsicherheit an erster Stelle stehen, um Reaktionsfähigkeit zu erreichen. Dies wird mit jedem Jahr wichtiger.