Reactive Programming stellt eine bedeutende Facette der modernen Softwareentwicklung dar. Sie basiert darauf, Software als eine Reihe von Interaktionen zu betrachten, die zwischen Ereignis-Produzenten, -Konsumenten und -Modifikatoren ablaufen. Richtig angewendet, bietet dieser Softwareentwicklungsansatz signifikante Vorteile, wenn es darum geht, Code zusammenzusetzen, zu erweitern und verständlich zu gestalten.
Reaktive Programmierung kann dazu beitragen, Ihre Anwendungsarchitekturen in einer Vielzahl von Szenarien zu optimieren. Etwa in den Bereichen:
asynchrone Programmierung,
Benutzeroberflächen und
verteilte Systeme.
Im Folgenden lesen Sie, was Sie zum Thema Reactive Programming wissen sollten.
Reactive Programming - Definition
Reactive Programming ist ein Softwareentwicklungsparadigma, das Inputs und Outputs als Event-Streams (Ereignisströme) modelliert. Mit Hilfe dieses Ansatzes können Entwickler Anwendungen in einem deklarativen, funktionalen und standardisierten Stil erstellen.
Das Herzstück sind dabei die Ereignisströme: Sie können jede Art von Datenfluss darstellen, von Netzwerkaktivitäten bis hin zu den Zeilen einer Textdatei. Jedes Mal, wenn eine Datenquelle diskrete Ereignisse auf der Grundlage von Elementen in den Daten auslösen kann, lässt sie sich als reaktiver Stream verpacken oder modellieren.
Mit Ereignisströmen können Sie Input- und Output-Quellen als zusammensetzbare Elemente verbinden und sie gleichzeitig an jeder Stelle der Kette manipulieren. So werden Anwendungen zu einem Netzwerk von Streams - mit Produzenten, Konsumenten und Modifikatoren.
Die reaktive Programmierung ähnelt in der Praxis der funktionalen, beziehungsweise ist von ihr inspiriert. Im Folgenden ein kleines Beispiel für ein reaktives Programm in JavaScript:
// Create an event stream from a text input keypress
const input = document.getElementById('myInput');
const keypressStream = rxjs.fromEvent(input, 'keypress');
// Apply operators to the stream
const filteredStream = keypressStream.pipe(
rxjs.operators.map(event => event.key),
rxjs.operators.filter(key => key !== ' '),
rxjs.operators.throttleTime(500) // Throttle keypress events to a maximum of one event per 500ms
);
// Subscribe to the stream and handle the events
filteredStream.subscribe(key => {
console.log('Keypress:', key);
});
Dieser Code überwacht eine Texteingabe auf Tastendrucke und verwendet die reaktive JavaScript-Bibliothek RxJS, um diese in einen Ereignisstrom zu verwandeln. Dazu nutzt er die reaktiven Operatoren map
, filter
und throttleTime
. Sie eliminieren mit dem Code zudem Leerzeichen und drosseln die Event-Frequenz auf 500 Millisekunden. Schließlich wird ein Abonnement für den Stream erstellt, das die umgewandelten Ereignisse auf der Konsole ausgibt. Dieses Beispiel können Sie hier live einsehen.
Reaktive Programmierung - Vor- & Nachteile
Event Streams sind ein klares und portables Mittel, um Datenströme darzustellen. Durch die Verwendung von Ereignisströmen wird der Code deklarativ und nicht imperativ, was in Sachen Composability und Verständlichkeit ein großer Gewinn sein kann. Zudem sind reaktive Systeme darauf ausgelegt, Datenströme asynchron zu verarbeiten. Das macht sie einerseits skalierbar und andererseits auch hinsichtlich der Concurrency optimierbar.
Die Kehrseite der Medaille: Reactive Programming ist mit erheblichem Lernaufwand verbunden - es gibt entsprechend wenige echte Experten auf diesem Gebiet. Ein weiterer Nachteil: Hat man erst einmal erfasst, wie leistungsfähig der reaktive Ansatz ist, könnte man versucht sein, ihn als Allheilmittel zu betrachten.
Reactive Programming - Elemente
Auf hoher Ebene basiert Reactive Programming auf mehreren Kernelementen:
Observables sind das konkrete Mittel, um beliebige Ereignisströme als wiederverwendbare Komponenten zu modellieren. Observables haben eine klar definierte API, die Ereignisse, Fehler und Lebenszyklusereignisse erzeugt, welche andere Komponenten abonnieren oder ändern können. Im Wesentlichen macht der
Observable
-Type aus einem Ereignisstrom eine portable, programmierbare Einheit.Observers sind das Gegenstück zu Observables. Sie abonnieren die Ereignisse, die Observables erzeugen, und nehmen an ihnen teil. Das hat zur Folge, dass der Anwendungscode zu einer Inversion-of-Control-Position neigt, in der er Observables und Observer miteinander verbindet, anstatt sich direkt mit der Funktionalität zu befassen.
Operators sind das Äquivalent zu funktionalen Operatoren, die auch als Funktionen höherer Ordnung bezeichnet werden. Mit reaktiven Operators lassen sich die Events, die den Stream durchlaufen, auf vielfältige Weise manipulieren. Auch hier kann der Anwendungscode auf Distanz bleiben, indem die gewünschte Funktionalität "von oben" in die Datenströme injiziert wird - während die Operationen eng mit den Daten verbunden bleiben, die sie bearbeiten.
Scheduling
Ein weiterer wichtiger Komponententyp ist der Scheduler. Bei der reaktiven Programmierung unterstützt er dabei, zu managen, wie Events von der Engine behandelt werden. In unserem eingangs genannten Beispiel könnten wir die Engine zum Beispiel anweisen, die Operation mit einem asynchronen Scheduler auszuführen:
const filteredStream = keypressStream.pipe( rxjs.operators.observeOn(rxjs.asyncScheduler),
rxjs.operators.map(event => event.key), rxjs.operators.filter(key => key !== ' '),
rxjs.operators.throttleTime(500) );
Das Konzept des Schedulers ist eine leistungsfähige Methode, um das Verhalten von Streams zu definieren und zu steuern. Es gibt diverse Variationen in verschiedenen Frameworks. Darüber hinaus ist es auch möglich, eigene Implementierungen zu schreiben.
Backpressure
Ein weiteres wichtiges Konzept in Sachen Reactive Programming ist Backpressure. Im Wesentlichen beantwortet es die Frage: Was passiert, wenn zu viele Ereignisse zu schnell eintreten, so dass das System sie nicht verarbeiten kann?
Eine Backpressure-Strategie unterstützt dabei, den Datenfluss zwischen Ereignisproduzenten und -konsumenten zu managen und sicherzustellen, dass letzterer die Menge der eingehenden Events verarbeiten kann, ohne überfordert zu werden. Dabei gibt es mehrere allgemeine Ansätze - beispielsweise:
Dropping: Wenn sich die Events stauen, werden sie "weggeworfen". Sobald der Konsument in der Lage ist, mehr zu verarbeiten, werden die aktuellsten Ereignisse geliefert. Hier liegt der Fokus auf Lebendigkeit.
Buffering: Eine Warteschlange mit unverarbeiteten Ereignissen wird erstelllt und schrittweise an den Konsumenten übergeben, sobald dieser dazu in der Lage ist, sie zu verarbeiten. Der Fokus: Konsistenz.
Throttling: Die Rate der Ereignisbereitstellung wird durch diverse Strategien wie Zeitdrosselung, Zähldrosselung oder Token-Buckets verringert.
Signaling: Eine Möglichkeit wird geschaffen, um dem Event-Produzenten den Backpressure-Status-Quo mitzuteilen, damit dieser entsprechend reagieren kann.
Backpressure-Strategien können sich entsprechend der Bedingungen dynamisch verändern. Wenn wir unser Beispiel um eine zählbasiertes Buffer Backpressure Handling erweitern wollten, würde das folgendermaßen funktionieren:
const filteredStream = keypressStream.pipe(
rxjs.operators.throttleTime(500), // Throttle keypress events to a maximum of one event per 500ms
rxjs.operators.observeOn(rxjs.asyncScheduler),
rxjs.operators.map(event => event.key),
rxjs.operators.filter(key => key !== ' '),
rxjs.operators.bufferCount(5)
);
Natürlich ist Backpressure für dieses Beispiel nicht erforderlich, vermittelt aber eine Vorstellung davon, wie es funktioniert. Die Anzahl der Tastenanschläge ist auf 500 Millisekunden begrenzt. Sobald fünf dieser Anschläge erfolgt sind, werden sie an den Abonnenten weitergeleitet, um sie auf der Konsole auszugegeben. Dieses Beispiel können Sie hier live einsehen.
Reactive Frameworks
Im Bereich Reactive Programming gibt es zahlreiche Frameworks und Engines für diverse Sprachen und Plattformen. Das Vorzeigeprojekt in diesem Bereich ist ReactiveX, dessen Standardspezifikationen in allen bedeutenden Programmiersprachen implementiert ist.
Darüber hinaus gibt es weitere Reactive Frameworks - zum Beispiel:
das Reactor-Projekt (basiert auf der JVM),
Spring WebFlux (basiert auf Reactor),
das Play-Framework (Java, Scala),
Vert.x (für Java, Kotlin und Groovy),
Akka Streams (Java),
Trio (Python) oder
Nito.AsynxExec (.Net).
Einige Sprachen haben auch eine gute reaktive Unterstützung eingebaut. Go mit goroutines ist nur ein Beispiel. (fm)
We didn't invent much
— Mario Fusco ???????? @mariofusco@jvm.social (@mariofusco) March 5, 2020
Divide et impera?microservices
Testis unus, testis nullus?high availability
Pacta sunt servanda?API
Mutatis mutandis?Agile
Carpe diem?reactive programming
Alea jacta est?immutability
Mors tua, vita mea ?circuit breaker
Ad libitum?horizontal scaling pic.twitter.com/4TE1xbZBL0
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.