Suche
Alles im Fluss

Mit JavaFX geht es flux: Flux-Architektur mit JavaFX

Manuel Mauky

© iStockphoto.com/fabioderby

In den letzten Jahren hat Facebook mit seiner JavaScript-Bibliothek React.js für einiges Aufsehen bei Frontend-Entwicklern gesorgt. Denn bei React werden einige altbekannte Muster und Best Practices kritisch hinterfragt, verworfen und neu gedacht. Mit der Flux-Architektur bietet Facebook auch einen Architekturansatz für React.js als Alternative zum klassischen Model View Controller an. Die Grundidee dieser Architektur ist aber nicht auf React oder JavaScript beschränkt, sondern kann auch für JavaFX-Entwickler interessant sein.

Zu den größten Herausforderungen von Frontend-Entwicklern zählt der Umgang mit veränderlichen Zuständen. Im klassischen Model View Controller befindet sich der Applikationszustand im Modell. Aber auch UI-Komponenten an sich besitzen natürlich einen Zustand, der bei Änderungen im Modell gezielt aktualisiert werden muss. Gezielt bedeutet hier, dass nur die Komponenten aktualisiert werden, die von der konkreten Änderung der Anwendungsdaten auch tatsächlich betroffen sind. Aber auch die andere Richtung der Kommunikation ist wichtig: Der Nutzer interagiert mit der View, um letztlich wiederum den Applikationszustand zu verändern. Wie die konkrete Ausgestaltung dieser Aspekte aussieht, ist Gegenstand der verschiedenen MVC-Varianten.

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

Im Web ist es ähnlich. Seit Ajax werden Daten dynamisch vom Server geladen und gezielt in den DOM-Baum der Seite eingefügt. Während das für einfache Beispielanwendungen mit purem JavaScript machbar ist, kommt man bei komplexeren Anwendungen kaum ohne ein Framework aus, das die DOM-Manipulation einfacher macht. Vor Ajax sah die Welt dagegen noch einfach aus. Bei geänderten Daten musste die gesamte Webseite neu geladen und gerendert werden. Der alte clientseitige Zustand in Form des DOMs wurde verworfen und durch einen komplett neuen ersetzt. In gewisser Weise bringen React und die Flux-Architektur dieses einfache Denkmodell wieder zurück in die Gegenwart.

Bei Flux befinden sich der Anwendungszustand und die Logik zum Verändern dieses Zustands in so genannten Stores. Stores sind allerdings nicht spezifisch für eine Ansicht, sondern repräsentieren eine fachliche Einheit. Views sind bei Flux weitestgehend zustandslos. Sie zeigen lediglich die Daten der Stores an. Ändern sich die Daten des Stores, wird die View neu gezeichnet. Entwickler müssen sich keine Gedanken darüber machen, welche UI-Komponente gerade welche Information anzeigt und welche UI-Updates notwendig sind, um die Oberfläche wieder auf den aktuellen Stand zu bringen. Stattdessen wird nur definiert, wie der Anwendungszustand auf UI-Komponenten abgebildet wird. Gedanklich nähert man sich damit den Ideen der funktionalen Programmierung an, die sonst ja eher selten mit dem Frontend in Verbindung gebracht wird.

Tatsächlich gibt es natürlich gute Gründe, warum wir heute nicht mehr ständig den Refresh-Button des Browser benutzen, sondern von modernen Anwendungen erwarten, dass sie sich dynamisch anpassen. Hier kommt React ins Spiel: Diese Bibliothek stellt dem oder der Entwickler/in ein API zur Verfügung, um gedanklich und konzeptionell nach dem oben beschriebenen Wegwerfen-und-Neuzeichnen-Modell zu entwickeln. Dazu haben React-Komponenten eine render-Funktion, die HTML zurückliefert, das die Darstellung der Komponente definiert. Bei jeder Datenänderung wird diese render-Funktion erneut ausgeführt.

Unter der Haube wird aber nicht wirklich immer alles neu gezeichnet. Stattdessen arbeitet React mit einem virtuellen DOM-Baum, der aus leichtgewichtigen JavaScript-Objekten besteht. React berechnet damit die DOM-Operationen, die mindestens notwendig sind, um den neuen Zustand tatsächlich im Browser darzustellen.

Dieses Vorgehen bietet entscheidende Vorteile: Als Entwickler arbeitet man mit einem einfachen Gedankenmodell, bei dem lediglich definiert werden muss, wie Daten auf HTML abgebildet werden. Über das eigentliche Rendering und die DOM-Manipulationen braucht man sich keine Gedanken zu machen. Das tatsächliche Rendering ist durch den virtuellen DOM darüber hinaus erstaunlich performant, da beispielsweise überflüssige DOM-Operationen vermieden werden. React selbst ist eine reine View-Bibliothek. Um React-Komponenten mit Flux zu verbinden, wird beim Store ein Listener registriert, der das Neuzeichnen der Komponenten auslöst, wenn sich der Store-Zustand aktualisiert hat.

W-JAX
Christian Schneider

Schlimmer geht immer – eine Auswahl der Top-10-Hacks der letzten Jahre

mit Christian Schneider (Christian Schneider IT-Security)

Die wesentliche Eigenschaft von Flux ist, dass es sich auf einen unidirektionalen Datenfluss beschränkt. Daten fließen nur vom Store zur View, nicht jedoch in die andere Richtung. Bei Interaktionen des Nutzers darf die View also nicht direkt den Zustand des Stores verändern, wie dies bei einigen MVC-Varianten der Fall ist. Stattdessen werden sämtliche Aktionen des Nutzers explizit modelliert. Im Sprachgebrauch von Flux wird dafür der Begriff „Actions“ benutzt, Java-Entwickler können hier aber auch an das bekannte Command Design Pattern denken. In einer To-do-Listen-Anwendung könnte man sich beispielsweise eine AddTodoItemAction oder eine MarkAsDoneAction vorstellen. Diese können jeweils noch Daten enthalten, die für die Aktion wichtig sind, wie die ID der abzuschließenden Aufgabe.

Abb. 1: Da der Datenfluss bei Flux ist unidirektional ist, darf die View nicht direkt den Zustand des Stores ändern

Abb. 1: Da der Datenfluss bei Flux ist unidirektional ist, darf die View nicht direkt den Zustand des Stores ändern

Views können als Reaktion auf Nutzerinteraktionen neue Actions erzeugen. Diese werden vom so genannten Dispatcher an sämtliche Stores in der Anwendung weitergeleitet. Stores können nun selbstständig entscheiden, auf welche Actions sie reagieren möchten und wie sie ihren internen Zustand basierend auf den eintreffenden Actions verändern. Bei Änderungen des Stores wird nun wiederum die View aktualisiert, womit sich der Kreis schließt.

 

Mit JavaFX geht es flux

Auch wenn Flux gezielt für React.js entwickelt wurde, ist die Grundidee natürlich nicht auf diese Bibliothek und auch nicht auf Webanwendungen beschränkt. Die versprochenen Vorteile, wie das einfache Programmiermodell, nachvollziehbarer Datenfluss und die verstärkte Nutzung von funktionalen Konzepten, sind natürlich auch für andere Entwickler, beispielsweise aus dem Desktop-Java-Bereich mit JavaFX, verlockend.

Versucht man allerdings das Muster eins zu eins mit JavaFX umzusetzen, stößt man relativ schnell an Grenzen. Das JavaFX-Äquivalent zum DOM-Baum heißt Scene Graph. Man könnte nun eigene Komponenten entwickeln, die ebenfalls bei jeder Datenänderung einen kleinen Scene Graph erzeugen, der in der Ansicht ersetzt wird. Allerdings ist dieses Vorgehen bei JavaFX nicht performant, sondern sorgt bereits bei kleinen Beispielen für ein merkliches Flackern. JavaFX ist relativ stark zustandszentriert und harmoniert daher eher mit klassischen Patterns wie Model View Controller und besonders Model View ViewModel (MVVM). Zwar arbeiten bei JavaFX unter der Haube vergleichbare Mechanismen wie der Virtual-DOM bei React, Entwicklern steht aber kein API zur Verfügung, um leichtgewichtig und performant bei jedem Update die komplette Ansicht neu aufzubauen.

JavaFX bietet dafür aber andere Features, mit denen sich die Flux-Architektur nachbilden lässt. Dazu zählen vor allem das Data-Binding-API und die reaktiven Streams, die durch Drittbibliotheken wie RxJava oder das hervorragende ReactFX – das zwar ähnlich wie React.js heißt, damit aber nichts gemein hat – nachgerüstet werden können.

Lesen Sie auch: JavaFX Sixpack: Neues von Jpro, Scene Builder, DromblerFX, FXGL, TornadoFX, JITWatch

Die Idee ist folgende: Wir bilden den Zustand des Stores mit JavaFX-Properties ab und geben diese als Read-only Properties nach außen. Die View hängt sich mit unidirektionalem Data Binding an die vom Store bereitgestellten Properties und zeigt damit stets den aktuellen Anwendungszustand an. Damit passiert hier zwar kein komplettes Re-Rendering, der Datenfluss ist aber ebenfalls klar und nachvollziehbar. Für alle Action-Typen werden dem Command Pattern folgend eigene Klassen angelegt, die ein gemeinsames Marker-Interface implementieren. Als Dispatcher kommt ein reaktiver Stream zum Einsatz, in den die Views neue Actions einbringen können. Der oder die Stores subscriben den Action Stream und können somit auf neu eintreffende Actions reagieren.

Um die Idee zu verdeutlichen, soll ein Minibeispiel mit einem Zähler dienen, der durch zwei Buttons hoch und runter gezählt werden kann. Fachlich sind zwei Aktionen vorgesehen: IncreaseAction und DecreaseAction, die jeweils den zu verändernden Betrag als Int-Wert enthalten und als immutable Klasse implementiert sind. In Listing 1 ist die IncreaseAction zu sehen.

public class IncreaseAction implements Action {
  private final int amount;
  public IncreaseAction(int amount) {
    this.amount = amount;
  }
  public int getAmount() {
    return amount;
  }
}

Der Store (Listing 2) enthält den aktuellen Zählerwert als ReadOnlyIntegerWrapper, einer Spezialisierung der normalen IntegerProperty, der schreibgeschützt nach außen gereicht werden kann. Der Store bekommt als Abhängigkeit den Action Stream als Konstruktorparameter. In diesem Beispiel wird die Klasse EventStream aus der Bibliothek ReactFX benutzt. Wer lieber RxJava nutzen möchte, würde hier Observable verwenden.

Mittels filter wird der Stream, der die einkommenden Actions enthält, auf die für den Store interessanten Action-Typen reduziert und abonniert. Trifft eine entsprechende Action ein, wird die Zähler-Property hochgezählt. Hierdurch wird wiederum automatisch auch die View aktualisiert.

public class CounterStore {
  private ReadOnlyIntegerWrapper counter = new ReadOnlyIntegerWrapper();

  public CounterStore(EventStream<Action> eventStream) {
    eventStream.filter(a -> a instanceof IncreaseAction)
               .cast(IncreaseAction.class)
               .subscribe(this::increase);

    eventStream.filter(a -> a instanceof DecreaseAction)
               .cast(DecreaseAction.class)
               .subscribe(this::decrease);
  }

  private void decrease(DecreaseAction action) {
    counter.setValue(counter.get() - action.getAmount());
  }

  private void increase(IncreaseAction action) {
    counter.setValue(counter.get() + action.getAmount());
  }

  public ReadOnlyIntegerProperty counter() {
    return counter.getReadOnlyProperty();
  }
}

Auch in punkto Testbarkeit ist diese Implementierung günstig. Der Store ist die Komponente, die besonders testbedürftig ist, enthält sie doch sämtlichen fachlichen Code. Im Unit-Test kann, wie in Listing 3 zu sehen, nun einfach geprüft werden, wie sich der Store bei bestimmten Actions verhält.

@Test
public void test() {
  EventSource actionStream = new EventSource<>();
  CounterStore store = new CounterStore(actionStream);

  // given
  assertThat(store.counter()).hasValue(0);

  // when
  actionStream.push(new IncreaseAction(1));

  // then
  assertThat(store.counter()).hasValue(1);
}

Ein weiteres Beispiel: Fachliche Aktionen modellieren

Weitere Beispiele sind im GitHub Repository FluxFX zu finden. Neben Beispielen enthält das Repository auch einige Hilfsklassen und Interfaces, die auch für eigene Anwendungen verwendet werden können. Das Projekt versteht sich aber nicht unbedingt als ein neues JavaFX-Framework, sondern eher als Spielwiese, um die Ideen der Flux-Architektur mit JavaFX auszuprobieren und weiterzuentwickeln.

Abb. 2: To-do-Listen-Anwendung mit JavaFX und Flux

Abb. 2: To-do-Listen-Anwendung mit JavaFX und Flux

Eines der Beispiele ist eine To-do-Listen-Anwendung, die dem aus der JavaScript-Welt bekannten TodoMVC nachempfunden ist (Abb. 2). Mit der Anwendung lassen sich Aufgaben erfassen und anschließend abhaken. An diesem Beispiel können einige Aspekte näher betrachtet werden, die bei dieser Technologiekombination auftauchen.

Generell stellt sich zum Beispiel die Frage, wie feingranular Actions geschnitten werden sollten und ob wirklich sämtliche Interaktionsmöglichkeiten über den Action-Mechanismus abgebildet werden. In der To-do-App wird der Löschen-Button eines jeden Eintrags beispielsweise immer erst dann angezeigt, wenn der Nutzer mit der Maus über den jeweiligen Eintrag fährt. Sollte man hier bei jedem Mouseover-Event eine neue Action erzeugen und die Information, für welche Einträge die Löschen-Buttons sichtbar sind und für welche nicht, im Store halten? Oder ist es hier vertretbar, mit dem Einzeiler deleteButton.visibleProperty().bind(root.hoverProperty()) einfach die entsprechenden Eigenschaften per JavaFX Data Binding direkt miteinander zu verknüpfen? Hier muss abgewogen werden, ob im Einzelfall doch veränderlicher Zustand in den Views eingeführt werden soll.

Ein anderer interessanter Aspekt im To-do-Listen-Beispiel ist das Verhalten der Checkboxen. Im oberen Bereich gibt es eine Hauptcheckbox, mit der sich alle Einträge auf „Abgeschlossen“ oder „Offen“ setzen lassen. Gleichzeitig soll diese Hauptcheckbox aber auch auf die Checkboxen der einzelnen Einträge reagieren und deren Zustand darstellen. Schließt der Nutzer alle Einträge einzeln ab, soll am Schluss auch die Hauptcheckbox einen Haken bekommen, sobald die letzte Aufgabe abgeschlossen wurde. Wird anschließend allerdings der Haken bei einem einzelnen Eintrag wieder entfernt, soll auch der Haken der Hauptcheckbox entfernt werden.

Versucht man dieses Verhalten allein als Zusammenhang zwischen den Checkboxen selbst zu modellieren, stößt man schnell an Grenzen. Der Zustand einer Checkbox lässt sich nicht alleine aus dem Zustand der jeweils anderen Checkboxen bestimmen. Stattdessen ist zur korrekten Implementierung des gewünschten Verhaltens die konkrete Nutzerinteraktion entscheidend. Nicht die Frage „Welche Checkbox ist aktiv?“, sondern „Welche Checkbox wurde geklickt?“ bestimmt über den Zustand der Checkboxen. Dies entsprich genau der Idee von Flux: Fachliche Aktionen werden explizit modelliert und das entsprechende Verhalten im Store implementiert.

Ähnliche Ansätze

Der Ansatz hinter der Flux-Architektur dürfte dem einen oder der anderen auch von anderer Seite her bekannt vorkommen. Beim Command-Query-Responsibility-Segregation-Muster (CQRS) wird ein ähnliches Vorgehen gewählt. Auch hier werden fachliche Aktionen explizit mittels Command Pattern modelliert. Auch der Datenfluss ist bei CQRS unidirektional. Bei CQRS wird allerdings eine strikte Trennung in ein Modell für Lesezugriffe und eines für Schreiboperationen durchgeführt, die so bei Flux nicht vorhanden ist.

Fazit und Ausblick

Der unidirektionale Datenfluss, die Read-only-Eigenschaft der Stores und die explizite Modellierung von Nutzerinteraktionen bringen einige Vorteile mit sich. Vor allem die Nachvollziehbarkeit der Vorgänge innerhalb der Anwendung wird verbessert. Durch die Verwendung von reaktiven Streams als Medium für den Transport der Actions ist es beispielsweise ein Leichtes, zu Debugging-Zwecken zu überprüfen, wann welche Actions erzeugt und weitergeleitet werden. Die Verarbeitung der Actions durch die Stores wiederum lässt sich einfach per Unit-Test überprüfen. Und schließlich lässt sich durch Data Binding auch die Anbindung der View an die Stores vernünftig umsetzen.

Was fürs Auge: JAX TV – Moderne Gestaltung für den Java-Desktop: Swing, JavaFX, SWT, HTML5?

Spannend an der Flux-Architektur ist vor allem die Annäherung an die Welt der funktionalen Programmierung. Zwar ist Flux an sich noch nicht vollständig funktional, die ersten Schritte sind aber bereits gegangen. Und wer die Entwicklung innerhalb der React-Community aufmerksam beobachtet, der weiß, dass vor allem in dieser Richtung noch einiges möglich ist. So existieren bereits Weiterentwicklungen von Flux, wie das Redux-Projekt, das einen vollständig funktionalen Ansatz wählt.

Diese und weitere Ansätze auch mit JavaFX auszuprobieren, wird in Zukunft spannend sein. Vor allem, da für die Java-Plattform auch echte funktionale Programmiersprachen wie die Haskell-Variante Frege bereitstehen. Diese besitzt auch eine JavaFX-Integration, die ein großes Potenzial für die Frontend-Entwicklung hat. Die künftige Entwicklung in diesem Bereich wird in jedem Fall spannend.

Geschrieben von
Manuel Mauky
Manuel Mauky
Manuel Mauky arbeitet seit 2010 als Softwareentwickler bei der Saxonia Systems AG in Görlitz. Er ist vor allem im Frontend-Bereich aktiv, seit einiger Zeit vor allem mit JavaFX. Daneben interessieren ihn Themen wie Softwarearchitektur, funktionale Programmierung und Reactive Programming. Manuel ist Gründungsmitglied und Leiter der Görlitzer Java User Group. Twitter: @manuel_mauky
Kommentare

Schreibe einen Kommentar

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