Einfach und effizient: Reactive Programming mit JavaFX

Manuel Mauky

© Shutterstock/Dusit

Die Nutzer von heute stellen hohe Erwartungen an die Benutzeroberflächen ihrer Programme. Ein sofortiges Feedback auf Eingaben und stets aktuelle Informationen werden erwartet, auch ohne dazu ständig auf irgendwelche Aktualisieren- oder Übernehmen-Knöpfe drücken zu müssen. Reactive Programming ist ein Programmierparadigma, das wie geschaffen für die moderne GUI-Entwicklung ist. Im Folgenden soll das Konzept vorgestellt und der Einsatz mit JavaFX gezeigt werden.

Die Anforderungen an moderne Programmoberflächen stellen GUI-Entwickler vor nicht zu unterschätzende Herausforderungen, für die klassische Ansätze kaum noch geeignet sind oder zumindest einige Probleme bereiten. Das Problem ist, dass unsere Programmiersprachen und -paradigmen im Wesentlichen darauf ausgelegt sind, den Programmablauf zu kontrollieren und zu steuern: zuerst Daten einlesen, verarbeiten und dann ausgeben. Bei interaktiven Programmen kann der Programmablauf aber nicht mehr ausschließlich durch den Entwickler vorgegeben werden, da stattdessen Ereignisse wie Benutzereingaben oder Änderungen in externen Datenquellen über die zeitliche Abfolge des Programms bestimmen, über die der Entwickler keine Kontrolle besitzt.

Angular - Eine Einführung

Angular – eine Einführung

Manfred Steyer

In diesem Videotutorial erklärt Manfred Steyer alles, was man für einen professionellen Umgang mit Angular benötigt und zeigt anhand eines praxisnahen Beispiels, wie man Services und Dependency Injection integriert, Pipes zur Formatierung von Ausgaben nutzt, Komponenten mit Bindings verwendet, Angular-Anwendungen mit Modulen strukturiert und Routing gebraucht.

Die klassische Antwort auf diese Situation heißt Observer, Listener oder Callback. Es wird ein Stück Code registriert, das ausgeführt wird, sobald ein bestimmtes Ereignis, zum Beispiel eine Nutzereingabe, eintritt. Die Nachteile dieses Vorgehens können jedoch beträchtlich sein: Bei nicht trivialen Programmen ist der Programmfluss oft nur noch schwer zu erahnen, vor allem wenn Callbacks verschachtelt sind. Wann und in welcher Reihenfolge welcher Code ausgeführt wird, ist dann nicht mehr ohne Weiteres ersichtlich. Fehler und Bugs sind folglich nur schwer aufzuspüren.

Ein weiterer Aspekt, der Callbacks schwierig handhabbar macht, ist die Koordinierung des Programmzustands: Callbacks sind keine „puren“ Funktionen im Sinne der funktionalen Programmierung, da sie keinen Rückgabetyp besitzen, sondern ausschließlich über Seiteneffekte wirken, also z. B. den Zustand von (aus Sicht des Callbacks) globalen Variablen verändern und auch lesend auf den globalen Programmzustand zugreifen. Dies macht oft umständliche Prüfungen bezüglich des gegenwärtigen Programmzustands notwendig. Unter JavaScript-Entwicklern (und nicht nur bei diesen) ist diese Situation als „Callback-Hölle“ bekannt.

int a = 5;
int b = 3;

int c = a + b;
System.out.println(c) // 8

a = 4;
System.out.println(c) // ?

Das Paradigma „Reactive Programming“ stellt eine vielversprechende Alternative dar, mit der GUI-Programme effizienter entwickelt werden können. Seit einiger Zeit erlebt der Begriff einen gewissen Hype, tatsächlich ist das Paradigma aber schon älter. Ein häufiges Missverständnis in diesem Zusammenhang soll aber noch ausgeräumt werden, bevor wir uns Reactive Programming im Detail anschauen:

Obwohl der Begriff „Reactive Programming“ oft synonym zu „Functional Reactive Programming“ benutzt wird, ist das in dieser Form nicht ganz korrekt. Reactive Programming lässt sich z. B. sehr wohl auch mit objektorientierten Mitteln umsetzen, wie wir in diesem Artikel sehen werden. Richtig ist aber, dass die Entwicklung von reaktiven Programmiersprachen vor allem auf Basis von funktionalen Sprachen stattfand und dort eine große Bedeutung erfährt.

Lesen Sie auch: JavaFX-Anwendungen aus der Praxis: So schön kann ein Java UI sein!

Um Reactive Programming zu verstehen, lohnt sich der Blick auf nicht reaktive Programme. Schauen wir uns dazu das Listing 1 an. Das Beispiel ist für jeden Programmierer trivial, trotzdem wollen wir einmal genau hinschauen, was hier passiert. In einer imperativen Programmiersprache hat dieser Code folgende Semantik: Die beiden Variablen a und b repräsentieren jeweils einen bestimmten Speicherbereich, in den per Zuweisungsoperator die Werte 5 und 3 geschrieben werden. Anschließend werden die aktuellen Werte der beiden Variablen addiert und das Ergebnis wiederum in der Variable c ablegt. Das erste Sysout gibt folglich 8 aus. Anschließend wird wieder per Zuweisungsoperator der Wert der Variable a verändert. Diese Zeile hat aber nur Auswirkungen auf a, c wird davon nicht beeinflusst, daher gibt auch das zweite Sysout wieder 8 aus. Um das Ergebnis der Addition mit dem neuen Wert von a zu bekommen müssten wir die Additionsoperation erneut ausführen und den Wert von c im Speicher aktualisieren.

Doch was wäre, wenn wir eine Programmiersprache hätten, in der dieser Code eine andere Semantik besitzen würde? Wir würden die Anweisung int c = a + b so interpretieren, dass c stets die Addition der Variablen a und b enthält, auch dann wenn sich a oder b ändern. Statt einer Zuweisung stellen wir eine ständige Relation zwischen den Variablen her. Das zweite Sysout würde dann also ohne weiteres Zutun 7 ausgeben. Vereinfacht gesagt stellen reaktive Programmiersprachen genau diese Semantik bereit. Zwei Faktoren sind dazu notwendig: Erstens können Abhängigkeiten zwischen Variablen definiert werden und zweitens werden bei Änderungen einer oder mehrerer Variablen alle abhängigen Variablen automatisch aktualisiert und notwendige Berechnungen erneut ausgeführt. Darum kümmert sich jedoch die Programmierumgebung, nicht der Entwickler.

Java stellt eine solche Semantik freilich nicht direkt zur Verfügung. Allerdings wurden mit der Einführung des GUI-Frameworks JavaFX so genannte Properties und Data Bindings mitgeliefert, mit denen genau diese Art der Programmierung möglich wird. Listing 2 zeigt das obige Beispiel in der Binding-Variante mit JavaFX. Zugegebenermaßen ist der Code aber nicht mehr so kompakt, was u. a. an der fehlenden Unterstützung für Operator-Überladung bei Java liegt.

IntegerProperty a = new SimpleIntegerProperty(5);
IntegerProperty b = new SimpleIntegerProperty(3);

NumberBinding c = a.add(b);
System.out.println(c.getValue()) // 8

a.setValue(4);
System.out.println(c.getValue()) // 7

a und b sind nun vom Typ IntegerProperty. Sie repräsentieren jeweils einen Integer-Wert, der vom Entwickler gesetzt und gelesen werden kann. c dagegen ist vom Typ NumberBinding. Es kann nur gelesen, jedoch nicht gesetzt werden, da es das Ergebnis einer Binding-Operation repräsentiert. Es kann aber selbst wiederum als Quelle weiterer Binding-Operationen dienen. JavaFX bietet von Haus aus eine ganze Reihe von Operationen zum Konstruieren von Bindings an. Neben den Grundrechenarten sind beispielsweise auch Methoden zum Konstruieren von booleschen Bindings vorhanden. Die Anweisung c.isEqualTo(10) erzeugt beispielsweise ein BooleanBinding, das immer dann den Wert true annimmt, wenn c den Wert 10 besitzt.

Der interne Aufbau der JavaFX-Properties ist im Prinzip leicht erklärt. Jede Property wrappt eine Variable des entsprechenden Typs und hat damit die volle Kontrolle über Wertänderungen. Im ersten Schritt bieten Properties nun die Möglichkeit, Observer zu registrieren, um über Wertänderungen benachrichtigt zu werden. Beim Data Binding wird intern genau das getan. Mit der Anweisung a.add(b) wird unter der Haube eine Binding-Instanz angelegt, die Observer bei a und b registriert, sich über Wertänderungen benachrichtigen lässt und in diesem Fall die Addition erneut ausführt. Der Vorteil liegt nun aber darin, dass der Entwickler sich nicht mehr selbst um die Steuerung der Observer kümmern muss.

Wirklich spannend wird das Ganze aber erst, wenn man die hier definierten Properties und Bindings mit einer grafischen Oberfläche verknüpft. Da bei JavaFX alle Controls sämtliche ihrer Eigenschaften ebenfalls als Properties zur Verfügung stellen, könnten wir zum Beispiel zwei Textfelder anlegen, dessen aktuelle Werte wir per Data Binding mit unseren Properties a und b verknüpfen. Tippt der Nutzer Zahlen in die Textfelder ein, sind diese sofort auch in unseren Properties präsent, wodurch auch das Ergebnis-Binding aktualisiert wird. Dieses könnten wir nun mit einem Label verknüpfen, sodass es sofort sichtbar wird. Eine solche Anwendung braucht keinen Berechnen-Button mehr, stattdessen reagiert die Anwendung selbstständig auf die Eingaben des Nutzers. Das hier angegebene Programm ist natürlich nur ein extrem vereinfachtes Beispiel, das aber sicherlich das Potenzial des Paradigmas erkennen lässt.

JavaFX bietet eine solide Basis für reaktives Programmieren. Möchte man aber komplexere Dinge ausdrücken, bieten sich diverse zusätzliche Bibliotheken an. So kann man zwar mit Standardmitteln Bindings für die Grundrechenarten erzeugen, darüber hinaus gehende Operationen fehlen allerdings. Eine mögliche Erweiterung stellt die Open-Source-Bibliothek Advanced-Bindings dar, die vom Autor dieses Artikels entwickelt wurde. Sie enthält unter anderem Data-Binding-Methoden für sämtliche in java.lang.Math enthaltene Operationen, zum Beispiel für die Quadratwurzel oder Sinus und Cosinus.

Ein anderes Beispiel ist eine Binding-Methode für reguläre Ausdrücke. Sie erwartet als Argumente zum einen das RegEx-Pattern und zum anderen den zu prüfenden Text und gibt ein BooleanBinding zurück, das angibt, ob der Eingabetext zum Pattern passt. Selbstverständlich können beide Eingabewerte auch als StringProperties übergeben werden, sodass das Ergebnis stets aktualisiert wird, wenn sich einer der beiden Eingabewerte ändert. Dies kann für einfache Validierungsaufgaben benutzt werden (Listing 3). Dort wird es mit einem SwitchBinding kombiniert: Je nachdem, welchen Wert die StringProperty language enthält, wird ein anderes RegEx-Pattern für die Validierung einer Telefonnummer benutzt (über die Korrektheit der Patterns ließe sich natürlich trefflich streiten). Sobald der eingegebene Wert nicht mehr zum Pattern passt, wird eine Fehlermeldung sichtbar geschaltet, was sowohl durch Änderung des Eingabewerts als auch Änderung der Sprache geschehen kann.

import eu.lestard.advanced_bindings.api.StringBindings;
import eu.lestard.advanced_bindings.api.SwitchBindings;
...

StringProperty language = new SimpleStringProperty("DE");

final ObservableValue phonePattern =
        SwitchBindings.switchBinding(language, String.class)
            .bindCase("DE", lang -> "\\+?[0-9\\s]{3,20}")
            .bindCase("US", lang -> "^[2-9]\\d{2}-\\d{3}-\\d{4}$")
            .bindDefault(() -> "[0-9 ]{3,20}")
            .build();

TextField phoneNumberInput = new TextField();
Label errorMessage = new Label("Falsches Format");

final BooleanBinding valid = 
  StringBindings.matches(phoneNumberInput.textProperty(), phonePattern);

errorMessage.visibleProperty().bind(valid.not());

Eine weitere nützliche Bibliothek mit ähnlichem Ziel, die hier jedoch nur genannt und nicht im Detail vorgestellt werden soll, ist EasyBind. Genauer betrachten wollen wir nun aber die Bibliothek ReactFX, die vom gleichen Autor wie EasyBind stammt. Der Name lässt bereits auf den Anwendungsbereich des Reactive Programmings schließen, der Fokus liegt hier jedoch auf einer anderen Art von Reactive Programming als die bisher gezeigte. Bisher haben wir vor allem den Programmzustand mittels zeitlich variierender Werte abgebildet. Hier stellt JavaFX sehr gute Möglichkeiten zur Kombination und Komposition bereit. Applikationen müssen aber auch mit Ereignissen wie Mausklicks umgehen können. Hierfür stellt JavaFX lediglich die bekannten Observer bereit, eine Möglichkeit zur Komposition fehlt allerdings.

Diese Lücke füllt ReactFX. Dafür stellt es so genannte EventStreams bereit. Sie senden (emittieren) Events, auf die man sich „subscriben“ kann. Das Besondere ist die Möglichkeit zur Komposition: Der Entwickler kann Filter anwenden, mehrere Streams zusammenführen oder einen Stream auf einen anderen Stream abbilden (map).

Pane root = new Pane();

final EventStream<MouseEvent> clicks = 
EventStreams.eventsOf(root, MouseEvent.MOUSE_CLICKED);

clicks.filter(click -> click.getButton() == MouseButton.PRIMARY)
            .mapToBi(click -> Tuples.t(click.getX(), click.getY()))
            .map((x,y) -> new Circle(x, y, 10, Color.RED))
            .subscribe(circle -> root.getChildren().add(circle));

In Listing 4 wird zum Beispiel zunächst ein Stream von Mausklicks angelegt. Dieser wird zunächst gefiltert, sodass lediglich Rechtsklicks übrigbleiben. Dieser Stream von Rechtsklicks wird nun auf einen Stream von Koordinaten des jeweiligen Klicks abgebildet. Der Koordinatenstream wird wiederum auf einen Stream von zu zeichnenden Kreisen an der entsprechenden Position abgebildet. Im letzten Schritt werden die Kreise der Anzeige hinzugefügt. Das Programm zeichnet also bei jedem Rechtsklick einen Kreis an die entsprechende Stelle.

Hier werden auch die funktionalen Konzepte deutlich, die im Reactive Programming ihr Potenzial ausspielen können: Für jeden Filter und Abbildungsschritt können tatsächlich „pure functions“ benutzt werden, also Funktionen, deren Rückgabewert ausschließlich von ihrem Eingabewert abhängt und die keine Seiteneffekte besitzen. Ein Seiteneffekt in dem Sinne findet nur ganz am Schluss statt, wenn der resultierende Stream tatsächlich im GUI erscheint.

Das Benutzen von Filtern, Abbildungen usw. im Zusammenhang mit Streams kommt dem interessierten Java-Entwickler aber sicherlich noch aus einer anderen Richtung her bekannt vor: Genau diese funktionalen Konzepte haben mit dem neuen Streams-API auch Einzug in Java 8 gehalten. Es gibt jedoch einen wesentlichen Unterschied: Die Java-8-Streams sind nicht reaktiv! Man erzeugt einen Stream (z. B. aus einer Collection), führt entsprechende Filter und Abbildungen durch und reduziert den Stream auf das gewünschte Ergebnis. Dieser Reduktionsschritt ist terminal, anschließend kann die Stream-Instanz nicht mehr weiterverwendet werden.

Anders bei den EventStreams von ReactFX. Diese bleiben die ganze Zeit über aktiv. Treffen neue Events auf dem Stream ein, werden die definierten Verarbeitungsschritte erneut durchgeführt. Das Verhalten der Java-8-Streams ist aber kein Fehler bzw. eine Schwäche, im Gegenteil, es handelt sich lediglich um ein anderes Werkzeug für andere Problemstellungen. Gleich ist das prinzipielle Denken: Mengen werden mittels Operationen verarbeitet und auf andere Mengen abgebildet – eine typische Denkweise der funktionalen Programmierung.

Konzeptionell sehr viel näher ist dagegen die Bibliothek RxJava, die zusammen mit ihren Pendants für andere Programmiersprachen aus der ReactiveX-Familie vor einiger Zeit den gegenwärtigen Hype für Reactive Programming mit ausgelöst hat. Auch hier dreht sich letztlich alles um Event-Streams, die entsprechend der Anforderungen komponiert und zusammengesetzt werden. Und auch hier sind die bekannten Operationen für Filter, Abbildung, Kombination und Aggregation vorhanden. Der wesentliche Unterschied ist, dass RxJava ihren Fokus auf die asynchrone Verarbeitung der Streams setzt. Damit ist sie zum Beispiel hervorragend für verteilte Applikationen und Netzwerkanwendungen geeignet.

ReactFX setzt dagegen auf synchrone Event-Propagation und Single-Threaded-Verarbeitung, was es besonders für die GUI-Entwicklung prädestiniert. Darüber hinaus ist das API direkt für die Integration in JavaFX-Anwendungen optimiert.

RxJava besitzt mit dem Subprojekt RxJavaFX zwar ebenfalls erste Ansätze in dieser Richtung, aktuell ist die Integration aber noch nicht sehr weit fortgeschritten. Mit etwas Handarbeit lässt sich aber auch RxJava mit JavaFX kombinieren, beispielsweise um einen Service einzubinden, der als RxJava-basiertes API vorliegt.

Listing 5 zeigt ein einfaches Beispiel. Die Methode temperatureService liefert einen Stream von sekündlich aktualisierten Temperaturwerten, der von einem Thermometer oder einem Internetdienst stammen könnte. Diese Werte wollen wir in einem Line-Chart anzeigen. Dazu erzeugen wir zusätzlich einen Stream von aufsteigenden Zahlen. Mit der zip-Funktion kombinieren wir anschließend beide Streams und erzeugen einen Stream von Chartdaten, der zum Schluss mit dem JavaFX-Chart verknüpft wird. Das Chart wird dadurch automatisch immer aktualisiert, sobald ein neuer Wert vom Temperaturservice geliefert wird.

final Observable ascendingNumbers = 
Observable.interval(1, TimeUnit.SECONDS);
final Observable temperature = temperatureService();

final Observable<XYChart.Data<Number, Number>> dataObservable =
        Observable.zip(ascendingNumbers, temperature, XYChart.Data::new);


final LineChart<Number, Number> lineChart =
        new LineChart<>(new NumberAxis(), new NumberAxis());
XYChart.Series<Number, Number> series = new XYChart.Series<>();
lineChart.getData().add(series);

dataObservable.subscribe(data -> series.getData().add(data));

Fazit

Wir haben gesehen, dass Reactive Programming ein interessantes Thema ist, ganz besonders auch für GUI-Entwickler. Es ermöglicht die Vermeidung von komplizierten und fehlerträchtigen Callback-Gerüsten und setzt stattdessen den Fokus auf die Beschreibung von Abhängigkeiten zwischen Werten und ihrer automatischen Aktualisierung. Vor allem bietet Reactive Programming einen sehr interessanten Ansatz, um die objektorientierte und funktionale Programmierung zu kombinieren bzw. das Beste aus beiden Welten zu vereinen.

Auch wenn die Reactive-Programming-Community aktuell ihren Blick sicherlich klar auf Event-Stream-basierte Ansätze richtet, ist auch der klassische Ansatz interessant. Dass dazu nicht zwingend eine funktionale Programmiersprache notwendig ist, sondern auch geeignete Abstraktionen über objektorientierte Design-Patterns ausreichen, ist eine interessante Erkenntnis.

JavaFX bietet dem Entwickler bereits gute Grundlagen, um reaktive Anwendungen zu programmieren. Darüber hinaus stehen einige Bibliotheken bereit, die dem Entwickler die Arbeit erleichtern. Ein Aspekt, der im Artikel nicht betrachtet wurde, ist die Frage nach einer geeigneten Frontend-Architektur. Hier steht zum Beispiel mit dem Model-View-ViewModel-Design-Pattern ein Architekturmuster bereit, das selbst Elemente von Reactive Programming beinhaltet und sehr gut für die Entwicklung von reaktiven GUI-Programmen geeignet ist.

Der vollständige Quellcode für alle Beispiele kann auf GitHub angeschaut und ausprobiert werden.

Aufmacherbild: Business man writing concept of efficiency von Shutterstock / Urheberrecht: Dusit 

Geschrieben von
Manuel Mauky

Manuel Mauky arbeitet seit 2010 als Softwareentwickler bei der Saxonia Systems AG in Görlitz. Er beschäftigt sich dort vor allem mit Java Enterprise und der UI-Entwicklung mit JavaFX. Aktuell arbeitet er an der Open-Source-Bibliothek mvvmFX zur Umsetzung von Model View ViewModel mit JavaFX.

Blog: http://www.lestard.eu

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.