Den Datenfluss in Web-Apps besser im Griff: Der Flux-Kompensator
Die Idee und der Entwurf für Flux wurden im Mai 2014 von Facebook-Entwicklern vorgelegt. Das Architekturmuster ist die Antwort darauf, wie sich Facebook zukünftig mit dem ebenfalls haus-gebackenen Framework React (siehe Ausgabe 41) vorstellt, komplexe und somit potenziell unüberschaubare Datenflüsse in Web-Applikationen abbilden zu wollen [1].
In Demos ist es immer ganz einfach: Eine Benutzereingabe erzeugt ein neues Element in einem JavaScript-Array, das dem Benutzer automatisch und formschön als aktualisierte To-do-Liste präsentiert wird. Der Vorfall ist fertig implementiert. Nicht so bei Facebook – dessen Abhängigkeiten in der Geschäfts- und Darstellungslogik sind weitaus komplexer. Auch wenn man keine sozialen Netzwerke baut: Die viel bemühte To-do-Liste entspricht in der Realität eher einer Liste von Produkten eines Onlineshops, mit Abhängigkeiten zum Warenkorb, ähnlichen Suchanfragen und mehr. Jeder UI-Entwickler, der mit derartigen Beziehungen zu tun hat, wird früher oder später feststellen, dass der Code schnell an Struktur und Übersichtlichkeit verliert: Abhängigkeiten werden angesichts eines fehlenden Entwurfs-Musters an ungeeigneten Controller- oder Router-Stellen implementiert, die Nachvollziehbarkeit der Ereignis-Behandlung geht verloren. Durch die Erzeugung marginal angepasster Kopien entsteht redundanter und wartungsintensiver Code.
Flux wurde also aus der Motivation heraus geboren, die Veränderung und den Fluss von Daten im Browser in eine wohldefinierte und nachvollziehbare Ablaufstruktur zu bringen. Flux wird manchmal fälschlicherweise in den exklusiven Zusammenhang mit React gebracht, doch tatsächlich beschreibt das Architekturmuster ganz allgemein ein Modell für Zuständigkeiten und Datenflüsse in JavaScript-Client-Architekturen.
Undirektionaler Datenfluss
Flux stellt als alternatives Architekturmuster zu MVC und der oft eingesetzten bidirektionalen Datenbindung einen Entwurf für den unidirektionalen Datenfluss vor. Es separiert strikt die Verarbeitung und somit Veränderung von Daten von ihrer Repräsentation. Es ergänzt das ebenfalls aus dem Hause Facebook stammende Framework React, ist aber auch losgelöst davon für die Integration in andere Frameworks und Umgebungen geeignet. Facebook selbst hat eine einfache Referenz-Bibliothek veröffentlicht. Was genau verbirgt sich hinter dem „Flux-Pattern“ und in welchen Fällen lohnt es sich, diese Architektur für das eigene Projekt zu evaluieren und einzusetzen?
Die Ausgangslage
Nehmen wir zunächst noch einmal die typische Verarbeitungsweise von Client-Architekturen nach dem MVC- beziehungsweise MVVM-Muster unter die Lupe. Als gedankliches Beispiel dient wieder die Suchfunktion eines Onlineshops: Die Formulareingabe eines Besuchers wird von einem Event-Handler entgegengenommen, mittels einer Controller-Funktion validiert und in eine geeignete Anfrage an das zugrunde liegende Modell umgewandelt, entweder durch die Ausführung eines Server-Requests oder durch die Filterung einer bereits im Browser geladenen Collection von Produkt-Modellen. Im einfachsten Fall landen die zurückgelieferten Suchergebnisse dann direkt in einer Variable der View, sodass sich diese mit den neuen Daten rendern lassen.
Soweit ein Interface wenige und in ihren Rollen weitgehend isolierte Modelle und Views abbilden soll, ist ein MVC-Muster ein einfacher und effektiver Weg zur Umsetzung. Oft ist das Beziehungsnetz von UI-Komponenten jedoch weitaus komplexer: Benutzer-Interaktionen haben Auswirkungen auf den Zustand anderer Modelle oder Modell-Aggregationen der Domäne, sodass verschiedene View-Komponenten der gesamten Darstellung einer Aktualisierung bedürfen. Ein wichtiger Aspekt ist hierbei, dass es sich um Beziehungen in den Präsentationsformen handelt, welche oft von der Geschäftslogik des zugrunde liegenden Datenbank-Modells abstrahieren. Um im Beispiel der Shop-Suche zu bleiben, denke man zum Beispiel an die Einblendung der letzten Suchanfragen oder Produktempfehlungen auf Basis des zuletzt gesuchten Produkts („Das könnte Sie auch interessieren…“).
Die Herausforderung ist nun, diese Quer-Beziehungen abzubilden, ohne dass der Gewinn an Wiederverwendbarkeit durch eine Separation von Darstellung, Verarbeitungslogik und Modell verloren geht. Hier klafft bei der Definition der MVC-Architektur eine Lücke und es fehlt an geeigneten Ausdrucksmitteln. Zudem ist die in vielen populären JavaScript-Frameworks eingesetzte Technik des „Two-Way-Data-Bindings“ zwar komfortabel und spart eine Menge Code, kann aber auch problematisch sein: Bei impliziter Bindung von View-Variablen an ein Objekt, welches seinerseits überwacht wird und Aktualisierungen an anderen Objekten durchführt, lassen sich Ursache und Wirkung von Daten-Mutationen manchmal schwer nachvollziehen. Im Resultat sieht man sich einer unerwünschten Kopplung zwischen verschiedenen Modellen und View-Komponenten gegenüber:
- View- bzw. Controller-Komponenten werden über ihre eigentlichen Zuständigkeiten, nämlich die Beschreibung ihrer eigenen Darstellungslogik hinweg aufgebläht. Der Code wird komplizierter und ist aufwändiger zu warten und zu testen.
- Sobald mehrere Modelle und Views sich gegenseitig bedingen, wird unter Umständen eine Kaskade von Ereignissen, Funktionsaufrufen und Aktualisierungen ausgelöst, welche schwer zurückzuverfolgen ist und im Extremfall sogar zu zirkulären Abhängigkeiten („Totschleifen“) führen kann. Im Folgenden Code ist beispielsweise ein überladener Event-Handler für die Shop-Suche in einer View-Komponente implementiert. Abhängigkeiten erfordern zusätzliche Modell-Aktualisierungen und View-Renderings.
var handleSearchTermSubmit = function() { var term = this.searchField.value.trim(); Products.fetch(term, function(products) { this.state.searchResults = products; this.render(); this.SearchHistoryView.state.items.push(term); this.StatusView.state.message = 'Hits: ' + products.length; }); Recommendations.fetch(term, function(products) { this.RecommendationsView.state.products = products; // [...] }); };
Listing 1
Die meisten JavaScript-Frameworks bieten eigene „Best Practices“ an, um diesen Problemen beispielsweise mit Beobachtern (Observern) und Regeln für die Event-Delegationen zwischen Parent- und Child-Views zu begegnen. Zusammengefasst: Ein komplexes Web-Interface nach einem MVC-Entwurf mitsamt eines nachvollziehbaren Ereignis-Systems zu implementieren, ist keine unlösbare Aufgabe, erfordert aber viel Disziplin und gedankliche Vorarbeit. Man wird zwangsläufig Strategien entwickeln, um den Daten- und Kommunikationsfluss zwischen Komponenten zu vereinheitlichen. Aber genau diese Vorarbeit haben andere bereits geleistet.
Geordneter Datenfluss
An diesem Punkt kommt nun endlich Flux als Alternative ins Spiel, welches ein Modell für den unidirektionalen Datenfluss und eine strikte Separation von Aktions-Verarbeitung, Daten-Manipulation und Daten-Repräsentation definiert.
Die Flux-Architektur sieht neben den bereits bekannten und in ihrer Rolle nur leicht veränderten Views vor allem „Actions“, einen „Dispatcher“ und „Stores“ als neue Komponenten vor. Der Datenfluss folgt dabei immer dem folgenden Verarbeitungsweg:
Action → Dispatcher → Store → View (→ Action …)
Alle Ereignisse, welche Einfluss auf den Zustand der Applikation nehmen, werden als Action definiert und über den Dispatcher verteilt, der als zentraler Event-Hub agiert. Die Stores bilden den Applikations-Zustand ab und stellen die Verarbeitungs-Logik zur Verfügung. Im Gegensatz zum „Model“ aus dem MVC-Muster sind Stores per Definition nicht darauf beschränkt, dedizierte Objekttypen zu repräsentieren. Sie können jede Art von Daten abbilden und verarbeiten, welche zu der von ihnen behandelten Teil-Domäne passen. Die reine Flux-Lehre sieht für die Erzeugung einer Action semantische Hilfsmethoden vor, bezeichnet als Action-Creators.
Dies soll mit Hilfe des Shop-Beispiels illustriert werden. Der Event-Handler für das Abschicken der Suchanfrage ruft einen Action-Creator nach dem „Fire & Forget“-Prinzip auf: Es gibt keine direkten Rückgabewerte und es werden auch keine Callback-Funktionen für eine etwaige Ergebnisverarbeitung übergeben. Stattdessen werden durch den Action-Creator eine oder mehrere Actions mitsamt Nutzdaten für den Dispatcher erzeugt. Bezogen auf die Shop-Suche könnte eine Action wie folgt definiert sein:
var actions = { SEARCHTERM_SUBMITTED: 'SEARCHTERM_SUBMITTED' }; dispatch(SEARCHTERM_SUBMITTED, { term: 'sphero' });
Listing 2
Der Name einer Action wird im Flux-Kontext als Konstante definiert und sollte ein bereits erfolgtes Ereignis beschreiben. Der Dispatcher meldet diese Action nun an alle Stores, sodass diese darauf reagieren und ihren Zustand aktualisieren können. Im Beispiel der Shop-Suche könnte ein Store „Products“ sowohl die Suchergebnisse als auch die empfohlenen Produkte verwalten, aber genauso gut kann sich eine Aufteilung in einen Store „ProductSearch“ und einen Store „ProductWarehouse“ anbieten: Diese würden nun unabhängig voneinander die Action ,SEARCHTERM_SUBMITTED‘ verarbeiten und adäquate Suchergebnisse bereitstellen. Nach der Verarbeitung melden die Stores „Change-Events“, um Veränderungen an den Daten bekannt zu geben. Als letzten Schritt schließlich rendern alle Views, welche Daten von einem oder mehreren Stores als State binden, ihre DOM-Repräsentation neu. Ein Action-Dispatch-Zyklus ist damit abgeschlossen.
Der Datenfluss kann als Kreisverkehr betrachtet werden: Daten bewegen sich stets in eine Richtung durch die zuständigen Teile der Architektur. Dies bedeutet, dass das System zu jedem Zeitpunkt definierte Zustände besitzt und eindeutige Zustandswechsel durchläuft, die sich reproduzieren und somit gut testen lassen.
Flux ist in seiner Grundidee kein gänzlich neuer Entwurf. Parallelen hat es etwa zu dem durch Young/Fowler beschriebenen Muster CQRS (Command-Query Responsibility Segregation) [2]. CQRS setzt im Kern auf eine klare Trennung von Modellen beziehungsweise Objekten zur Manipulation und Objekten zur Abfrage von Daten. Wichtig ist dabei, dass nie gleichzeitig eine Mutation und eine Rückgabe von Daten über ein und dasselbe Objekt beziehungsweise die darin ausgeführte Methode stattfinden darf. Flux unterscheidet zwar nicht explizit zwischen Kommandos und Abfragen, aber diese Trennung ergibt sich automatisch aus der Isolierung in Actions und dem davon entkoppelten Rückfluss von veränderten Daten über die Change-Events der Stores.
Traumpaar: React + Flux
Das ebenfalls bei Facebook aus der Taufe gehobene Framework React setzt den Schwerpunkt in der Kapselung des Zustands, der Logik und Lebenszyklus-Definition von View-Komponenten. Wer bereits die ersten Schritte mit React unternommen hat, wird wissen, dass Konzepte und Prototypen für Modelle und deren Datenbindung fehlen. So stellt sich bei der Entwicklung mit React nicht nur die Frage nach den richtigen Bibliotheken für zum Beispiel Routing und Server-Kommunikation, sondern auch für die Modellierung von Datenstrukturen und Datenflüssen – hier stellt Flux eine passende Komplementärarchitektur zu den React-Views dar.
Die Stores füllen die Lücke zur Abbildung der Modelle und Geschäftslogik. Views auf Kompositions-Level, so genannte „Controller-Views“, empfangen Daten der Stores und reichen diese selektiv als Properties an ihre Kind-Komponenten weiter. Das React-Prinzip „re-render everything“ bei Zustandsänderungen fügt sich nahtlos in den Flux-Kreislauf ein: Am Ende eines Zyklus werden sämtliche Views, welche den veränderten Zustand repräsentieren, automatisch neu gerendert. Das React-Team empfiehlt übrigens, die Server-Kommunikation innerhalb der Action-Creator durchzuführen: Nach einem Ajax-Request an eine API sollte eine Action mit einer Server-Antwort als Datenrückgabe erzeugt werden. Im Shop-Beispiel könnte diese etwa „PRODUCTFETCH_SUCCEEDED“ lauten, die zu übergebenden Nutzdaten bestünden aus einer Liste der gefundenen Produkte. Auch die Action „PRODUCTFETCH_FAILED“ ist denkbar, die Payload wäre dann die Fehlermeldung, welche ebenfalls nach Flux-Manier von einem oder mehreren Stores verarbeitet und über View-Bindungen dargestellt wird.
Immutability & Isomorphie
Zwei interessante Aspekte sollen an dieser Stelle noch Erwähnung finden. Zum einen das Konzept der „Immutability“: Hiermit ist das Paradigma gemeint, nach welchem Daten nie direkt mutiert werden, sondern sich immer nur als veränderte Kopie speichern lassen. Dies wird nicht nur der Lehre der reinen funktionalen Programmierung gerecht, sondern birgt gleichzeitig unschätzbare Vorteile wie eine „kostenlose Undo-History“ und drastisch verbesserte Testbarkeit. Das Projekt redux genießt diesbezüglich derzeit große Aufmerksamkeit und schlägt eine von Flux inspirierte Architektur kombiniert mit eingebauter Immutability vor [3].
Auch der Begriff der „Isomorphen Web-Anwendung“ taucht vermehrt auf. Als isomorph werden JavaScript-Anwendungen bezeichnet, die ganz oder teilweise client- und serverseitig ausgeführt werden können. Serverseitiges Rendering kann wichtig für SEO-Zwecke, aber auch bei Performance- oder UX-Problemen durch viele kleinteilige und damit kostspielige Rendering-Vorgänge im Browser sein. Bei React etwa ist es leicht möglich, Views komplett serverseitig in einer Node.js-Umgebung rendern zu lassen. Da Flux mit dem unidirektionalen Datenfluss die Zuständigkeiten der einzelnen Anwendungs-Teile gut separiert, bringt es eine der Grundvoraussetzungen für die Erstellung isomorphen Codes mit sich. Das Projekt fluxible stellt einen „Container“ mit Werkzeugen für die Implementierung isomorpher Flux-Anwendungen bereit und bietet auch Tools zur Bindung an React [4].
Fazit
Wer Web-Applikationen mit komplexen Beziehungen in der Präsentations-Logik entwickelt, sollte auf jeden Fall eine Flux-Architektur in Betracht ziehen. Insbesondere, wenn die Repräsentation und Veränderung von Daten an verschiedenen Stellen gleichzeitig und auf unterschiedlichen Wegen erfolgen kann, ermöglicht sie eine signifikante Vereinfachung in der Entwicklung und schließlich auch Test- und Wartbarkeit des Codes. Die individuelle Auslegung des Musters kann dabei variieren und es stehen eine Vielzahl quelloffener und gut dokumentierter Bibliotheken zur Verfügung, um Flux zum eigenen Projekt passend zu adaptieren [5].
Die Tatsache, dass die Idee für Flux den Entwickler-Köpfen aus dem Hause Facebook entspringt, bedeutet nicht, dass sich Flux nur sinnvoll im Zusammenhang mit React einsetzen ließe. Dennoch passt die Flux-Architektur besonders gut zum reaktiven Ansatz der React-Views. Selbst wenn Flux eines Tages in der jetzigen Variante überholt sein sollte: Es hat bereits den Stein ins Rollen gebracht, gewohnte Muster wie MVC und Derivate als Universalmuster in der Web-Entwicklung zu überdenken.