Reactive Programming – Mehr als nur Streams und Lambdas
Reactive Programming wird in einschlägigen Quellen oft als Kombination von Immutable (unveränderbaren) Streams und Propagation of Change oder als das nächste große Programmierparadigma beschrieben. Bei dem Ausdruck Propagation of Change, der Verbreitung von Änderungen, denkt man direkt an das Observer-Pattern, welches eine große Rolle bei Reactive Programming spielt.
Bei Reactive Programming dreht sich vieles um die sogenannten Observables, die man als Grundsteine des Reactive Programmings bezeichnen kann. Observables sollen einem System ermöglichen, ohne Zustand (state) auszukommen. Mit dem Konzept der Observables tauchen alte Bekannte wie Lambda-Expressions, map, reduce und filter auf.
Es existiert bereits ein Manifest, welches beschreibt, was ein Reaktives System ist: Responsiv, Widerstandsfähig, Elastisch und Nachrichtengetrieben. Reactive Programming wirft auch viele Fragen auf: Ist Reactive Programming überhaupt ein neues Programmierparadigma? Wie testen wir Reaktive Systeme, wenn es nicht wirklich einen Zustand gibt? Wer hat die Kontrolle über Reaktive Systeme, wenn alles auf ständiger Veränderung basiert?
Die allererste Frage, die sich beim Thema Reactive Programming stellt, ist: Was ist dieses Reactive Programming überhaupt? Hat es etwas mit einer Programmiersprache zu tun? Ist es ein Vorgehensmodell wie Scrum? Also läuft man mit all seinen Fragen los und erhofft sich eine schnelle Antwort von Google oder Wikipedia. Google verwirrt in diesem Fall relativ fix, da z. B. auf StackOverflow diskutiert wird, ob es nun Functional Reactive Programming heißt, oder einfach nur Reactive Programming. Der Einfachheit halber wird hier der Begriff Reactive Programming verwendet, um möglichst wenig Verwirrung zu stiften. Wikipedia verweist bei der Suche nach "reaktiv" auf die Begriffserklärung von "Reaktivität". Reaktivität im Sinne der Sozialwissenschaften ist die "Zustandsänderung des Erlebens und Verhaltens durch das Wissen, beobachtet zu werden" [1]. Später werden wir sehen, dass diese Beschreibung aus der Sozialwissenschaft sehr viel mit Reactive Programming zu tun hat. Sucht man weiter nach Reactive Programming oder "Reaktiver Programmierung", so findet man direkt zum gleichnamigen Artikel auf Wikipedia. Dieser ist recht kurz, bietet aber einen weiteren wichtigen Hinweis, was Reactive Programming sein kann: Ein Programmierparadigma.
Programmierparadigmen
Programmierparadigmen gibt es viele, z. B.: Iterative Programmierung, Prozedurale Programmierung, Objektorientierte Programmierung oder auch Funktionale Programmierung. Im Laufe eines Informatikstudiums hat man Kontakt zu vielen dieser Paradigmen. Um zu verstehen, was Reactive Programming ist, lohnt es sich, einen kleinen Abstecher über zwei dieser Paradigmen zu machen: Objektorientierte- und Funktionale Programmierung.
Objektorientierte Programmierung erblickte das Licht der Welt mit der Programmiersprache Smalltalk in den 70er Jahren. Alan Kay, einer der Designer und Entwickler dieser Sprache hat den Begriff "object oriented" als erster verwendet. Er beschreibt Objektorientierte Programmierung als "…Messaging, lokales Beibehalten und Schützen und Verbergen des Prozesszustands sowie spätestmögliche Bindung aller Dinge". Heutzutage ist dieses Programmierparadigma kaum noch wegzudenken. Große Vertreter der Objektorientierten Programmierung sind Java und C#. Diese Art zu programmieren hat verglichen mit Imperativer oder Prozeduraler Programmierung ganz neue Möglichkeiten eröffnet, wie das Wunder der Polymorphie oder komplexe Software-Architektur. 1994 brachten Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides (die Gang of Four [2]) das bekannte Buch "Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software" heraus. Noch heute wird dieses Buch immer wieder zu Rate gezogen, wenn über Objektorientierte Programmierung gesprochen wird.
Java und C# verwenden zusätzlich wichtige Elemente von Funktionaler Programmierung, die immer mehr an Popularität gewonnen hat. Funktionale Programmierung ist seit den 30er Jahren im Umlauf und ein ebenfalls kaum mehr wegzudenkendes Programmierparadigma, welches hauptsächlich auf dem Lambda-Kalkül basiert. Das Lambda-Kalkül beschreibt Funktionen und deren Auswertung. Mit Hilfe von Funktionaler Programmierung erhalten Programme eine mathematische Grundlage und werden beweisbar. Wichtige Vertreter der funktionalen Programmiersprachen sind z. B. Haskell und Erlang.
Reactive Programming verwendet viele Teile aus Objektorientierter und Funktionaler Programmierung und ist somit genaugenommen nicht mit einer neuen mathematischen Erkenntnis verbunden, sondern kann vielmehr als eine nächste evolutive Stufe der Programmierparadigmen betrachtet werden. Reactive Programming wird als Programmierung mit asynchronen, immutable Streams von Events bezeichnet. Das klingt zunächst nicht neu, hat jedoch große Auswirkungen darauf, wie wir unsere Software schreiben können. Diese Streams von Events können miteinander kombiniert und von mehreren Parteien abgehört werden. Dass ein Stream abgehört wird, lässt sofort die Vermutung nahe, dass hierbei das Observer-Pattern zum Einsatz kommt. Doch vorher ein konkretes Beispiel:
Arrays und Events
Betrachten wir ein Array mit zufälligen Zahlen:
int[] foo = {1,8,5,6,3};
Wendet man Funktionen aus der Funktionalen Programmierung auf dieses Array an, so lassen sich einfach Fragen wie: Was ist die größte Zahl in diesem Array, oder: Was ist die kleinste Zahl in diesem Array, beantworten. Man benutzt ganz einfach die Funktionen max oder min:
Arrays.stream(foo).max(); Arrays.stream(foo).min();
Aus dem Array lassen sich jederzeit Elemente entfernen oder hinzufügen. Vereinfacht gesagt kann immer wieder über das Array iteriert werden. Im Hintergrund werkelt dabei das Iterator-Pattern. Das Iterator-Pattern ist ein sehr nützliches Entwurfsmuster. Es ermöglicht einem Konsumenten, wann immer dieser es verlangt, die Elemente des Arrays abzurufen. Ab einem gewissen Punkt meldet das Iterator-Pattern, dass das Ende des Arrays erreicht ist und keine neuen Elemente mehr zur Verfügung stehen. Zusätzlich hat das Iterator-Pattern die Möglichkeit, einen Fehler zu werfen und dem Konsumenten mitzuteilen, dass etwas schiefgelaufen ist.
Doch wie handhaben wir einen Stream von Events bei dem wir, sobald ein Event auftritt, eine Funktion auswerten wollen? Jetzt kommt Reactive Programming ins Spiel. Wir können ähnlich zu dem oben genannten Beispiel mit statischen Werten, z. B. die Funktion max, auf einen Stream von Events anweden. In Java ermöglicht dies das Framework RxJava. RxJava kommt aus der Familie der sogenannten Reactive Extensions [3]. Die Reactive Extensions machen Gebrauch vom Observer-Pattern, welches die umgekehrte Form des Iterator-Pattern darstellt. Beim Observer-Pattern registriert sich ein Subscriber an einem Observable und bekommt jede Veränderung dieses Observables mit. Im Buch der Gang of Four wurden dabei jedoch zwei Dinge übersehen: Der Subscriber bekommt keine Benachrichtigung, wenn keine Events mehr kommen oder ein Fehler aufgetreten ist. Reactive Extensions führt beides ein und vereinigt diese Funktionen zusammen mit dem Observer-Pattern in einem Interface: dem Observable. Das Observable ist gleichwertig mit dem Iterator-Pattern, hat allerdings einen markanten Unterschied: Die Kontrolle über den Datenfluss wird vom Subscriber ganz aus der Hand gegeben und Daten bzw. Events werden dem Subscriber "gepusht". Damit ist es möglich, Programme zu schreiben, die (theoretisch) ohne jeglichen Zustand auskommen. Diese Observables lassen sich nach Lust und Laune mit Hilfe von Funktionen auswerten und kombinieren. Hier ein Beispiel von der Webseite rxmarbles [4], welches die Funktion zip angewandt auf zwei Streams von Events zeigt:
Die Seite rxmarbles ist sehr hilfreich, um die Kombination verschiedener Funktionen und Streams zu visualisieren. Dabei ist zu beachten, dass die Achsen, auf denen sich die Events (Murmeln) befinden, Streams darstellen sollen, zu sehen an den Pfeilen rechts im Bild. Die Events lassen sich beliebig verschieben und das Ergebnis wird unter der Funktionsbenennung dargestellt. Mir hat diese Webseite bei den ersten Gehversuchen mit Reactive Programming enorm weitergeholfen. Schreibt man die abgebildete Funktion in Java auf, so erhält man den Codeschnipsel
public Observable<String> zipObservables(Observable<String> o1, Observable<String> o2){ return o1.zipWith(o2, (x, y) -> x + y); }
Cold und Hot Observables, oder auch: Die heiße Kartoffel
Mit dem Observable Interface ist man allerdings nicht automatisch "reactive". Es existieren noch weitere Frameworks, die Reactive Programming umsetzen. In diesem Artikel wollen wir uns allerdings der Einfachheit halber um RxJava [5] kümmern. Das Observable Interface gibt einem das Rüstzeug in die Hand, um Reaktive Systeme zu schreiben. Doch bevor man sich mit diesem mächtigen Werkzeug auf den Weg macht, sollte man vorher verstehen, welche unterschiedlichen Arten von Observables existieren. Konkret geht es um Cold und Hot, also kalte und heiße Observables.
Cold Observables können mit einer Liste oder einem Array verglichen werden. Bei ihrer Erzeugung steht bereits fest, dass sie ein Ende haben. Der Umgang mit ihnen erfordert dementsprechend nicht viel Umstellung in der gewohnten Handhabung von Collections. Ihr Inhalt lässt sich allerdings nicht verändern. Der Zugriff auf Cold Observables gleicht also dem gewohnten Zugriff auf Listen oder Arrays. Dazu ein Beispiel:
return Observable.from(new Arrays.asList("a","b","c"));
Hier wird ein Observable direkt von einer Liste von Strings erzeugt. Danach kann das Observable nicht verändert werden, ist also immutable. Jedoch kann das Observable z. B. mit der oben eingeführten Methode zipObservables mit einem anderen Observable kombiniert werden, was wiederum ein neues Observable erzeugt.
Hot Observables dagegen haben einen anderen Charakter: Ihr Inhalt ist bei der Erzeugung noch nicht bekannt, genauso wie das Ende des Streams an Elementen. Auch dazu ein Beispiel:
Observable<Long> o = Observable.interval(1, TimeUnit.SECONDS);
erzeugt ein Observable, welches nach jeder Sekunde ein Event feuert, an welches man sich nun subscriben kann. Das würde dann so aussehen:
o.subscribe(x -> System.out.println(x));
Ausgegeben wird dann eine Liste von Longs, die nach jeweils einer Sekunde ein neues Element enthält und von 0 beginnend zählt. Ein vielleicht anschaulicheres Beispiel ist ein Stream an Aktienkursen, der fortlaufend Daten liefert. Subscribed man sich an diesem Stream, erhält man immer die aktuellen einlaufenden Kurse.
Diese Beispiele lassen sich sehr einfach implementieren und man bekommt ein Gefühl, wie sich Observables verhalten und wie man mit diesen umgeht. Wem das noch nicht reicht, dem empfehle ich, einen Blick in das Github-Repository learnrxjava von Jafar Husain [6] zu werfen. Dort finden sich viele Beispiele und Aufgaben anhand derer der Umgang mit Observables erklärt wird.
Reaktive Systeme
Hat man einmal die Grundlagen des Reactive Programmings gemeistert und sich mit dem Observable vertraut gemacht, kann man sich Gedanken über Reaktive Systeme machen. Und was macht man heutzutage in der Welt der Softwareentwicklung, wenn man etwas Neues wie Reactive Programming vorantreiben möchte? Man schreibt ein Manifest. Das passende Manifest zu Reactive Programming heißt "Reactive Manifesto" [7] und beschreibt, wie Reaktive Systeme aufgebaut sein sollten. Dabei geht es mehr um Software-Architektur, als um das Observable oder Observer-Pattern an sich.
Reaktive Systeme werden im Allgemeinen als schneller und robuster als herkömmliche Software beschrieben. Diese Eigenschaften sollen sich mit den vier Kernpunkten des Reactive Manifestos umsetzen lassen: Reaktive Systeme sind Antwortbereit (Responsive), Widerstandsfähig (Resilient), Elastisch (Elastic) und Nachrichtenorientiert (Message Driven).
Responsive bedeutet in diesem Kontext, dass das System eine angebrachte Quality of Service liefert. Antworten werden innerhalb einer akzeptablen Zeit gegeben und Fehler werden schnell erkannt. Ist ein System Responsive, so fühlt es sich für den Benutzer angenehm an, dieses zu benutzen und bindet auf längere Zeit an den angebotenen Service.
Resilient besagt, dass ein System im Falle eines auftretenden Fehlers immer noch Responsive bleibt. Dieses Verhalten kann z. B. mit Hilfe von Isolation und Replikation implementiert werden. Verschiedene Entwurfsmuster wie das Circuit Breaker Pattern oder das Saga Pattern können zu diesem Verhalten beitragen. Im Buch "Release it!" von Michael T. Nygard [8] wird das Circuit Breaker Pattern beschrieben: Anfragen an einen kritischen Service werden so verpackt, dass nach einer bestimmten Zeit eine Antwort erwartet wird. Kommt die Antwort nicht zurück, öffnet sich die Sicherung und die anfragende Partei weiß somit, dass der angefragte Service die Antwort in der erwünschten Zeit nicht übermitteln wird. Dieser Fehler kann anschließend schnell behandelt werden, ohne dass auf die Antwort des Services gewartet werden muss.
Elastic gibt an, dass das System skalieren muss. Unter verschiedener Last soll das Reaktive System einen konstanten Quality of Service liefern, was heutzutage mit Hilfe von Cloud-Diensten kein Problem mehr darstellt. Ist ein System elastisch, so lässt es sich möglichst kosteneffektiv betreiben.
Message Driven sollen Reaktive Systeme sein, um lose Kopplung zu ermöglichen. Eines der obersten Ziele, die man mit guter Software-Architektur erreichen möchte, ist bekanntlich eine lose Kopplung. Zusätzlich können Fehler an weitere Systeme mit Hilfe von Nachrichten schnell propagiert werden. Ein weiterer Vorteil der Nachrichtenorientiertheit ist, dass sich diese Systeme einfach überwachen lassen, indem man sich an die Nachrichten hängt.
Zusammenfassung
Man kann nun diskutieren, ob Reactive Programming wirklich neu ist: Für mich steht fest, dass Reactive Programming viele sinnvolle Bausteine aus Software-Architektur und modernen Techniken, wie asynchrone Streams und Funktionale Programmierung kombiniert, um ein Gefühl dafür zu vermitteln, dass Software auch anders gebaut werden kann. Man kann sich mit Reactive Programming von der Idee lösen, alle möglichen Zwischenstände irgendwo speichern zu müssen, indem einfach direkt die Änderungen propagiert werden. Alle Beispiele waren bisher in Java beschrieben, jedoch gibt es noch viele weitere Sprachen und Frameworks, mit denen sich Reactive Programming betreiben lässt:
- ReactJS [9], ein JavaScript-Framework, welches Reactive Programming für die Entwicklung von Webseiten benutzt und in letzter Zeit einen enormen Hype erlebt hat,
- Vert.x [10], ein Framework für die JVM, um Reaktive Systeme zu bauen und
- Akka [11], ein Framework, welches ins Leben gerufen wurde, um die genannten Eigenschaften eines Reaktiven Systems umsetzbar zu machen.
- Wikipedia: Reaktivität
- Wikipedia: Entwurfsmuster / Gang of Four
- ReactiveX
- Rxmarbles: Interactive diagrams of Rx Observables
- Github: ReactiveX/RxJava
- Github: jhusain/learnrxjava
- The Reactive Manifesto
- M. T. Nygard, 2007: Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers)
- ReactJS
- Vert.x
Informatik Aktuell: Jochen Mader: Vert.x 3: Reactive Microservices - akka