State of the Application 20.07.2016, 10:35 Uhr

Statusdaten im Blick

Wie ist der Stand der Dinge in Sachen Zustandsverwaltung bei Applikationen?
Kennen Sie das auch? Anwendungs- und UI-Entwicklung könnte so einfach und entspannend sein, wenn da nicht diese lästigen Daten wären, mit denen man sich ständig herumplagen muss. Fortlaufend ändern die sich. Immerzu muss man die Oberfläche aktualisieren, nur um den aktuellen Status wiederzugeben.
Und dann erst diese Ereignisse und Interaktionen. Alle Nase lang manipuliert jemand am internen Zustand herum und erwartet dann auch noch, daß sich das UI schnell mitändert. Und wehe, man vergisst mal einen Event Handler, schon funktioniert ein nötiger Refresh nicht mehr und der User reagiert völlig zu Recht verschnupft.
Zweifellos sind Daten das Rückgrat jeder Applikation. Schließlich heißt es nicht grundlos Datenverarbeitung. Besagte Daten kommen in vielfältiger Form daher. Um Daten dauerhaft aufzubewahren, werden sie in ein Speichermedium serialisiert. Das kann zum Beispiel eine eigene Datei sein, so wie das Dokument für diesen Artikel. Oder vielleicht eine Handvoll Datensätze in einer Datenbank. In real existierenden Anwendungen gibt es aber noch eine andere Art von Daten, die meist nicht in ein Speichermedium persistiert werden, oder allenfalls nur temporär.
Diese Daten beinhalten alle notwendigen Informationen, die die Applikation intern benötigt, um ihrer Aufgabe nachzukommen und den aktuellen Zustand zu beschreiben. Welcher Menüpunkt ist gerade verfügbar und welcher nicht? Wie ist der Name des aktuell geladenen Dokuments? Welcher Datensatz in der Liste ist gerade selektiert? Hat der Anwender das Spaltenlayout verändert oder die Sortierordnung der Tabelle geändert? All diese in der Regel flüchtigen Informa­tionsbröckchen fallen in die Kategorie Statusdaten.
Machen wir ein kurzes Gedankenexperiment. Welche Informationen müsste man sichern, um den Anwender in die Lage zu versetzen, auch in zwei Wochen noch genau an derselben Stelle weiterarbeiten zu können, wenn er jetzt den Computer herunterfährt oder das Browserfenster schließt?
Da wären zunächst die Informationen zum aktuellen Naviga­tionspfad. Eventuell ist auch ein aktives Projekt oder Dokument geladen, das bearbeitet werden soll. Die Position innerhalb des Dokuments beziehungsweise die angezeigten Datensätze wären sicher wichtig.
Hier stellt sich die Frage, ob man tatsächlich Verweise auf die Datensätze speichern sollte, oder doch lieber die Abfrage mit allen Parametern, mit denen diese Auflistung erzeugt wurde? Oder falls es sich doch um ein Dokument handelt: Welche Änderungen hat der Anwender vorgenommen, die noch nicht gespeichert wurden?
Bereits nach kurzem Nachdenken fallen uns für eine vergleichsweise einfache Applikation jede Menge Informationen ein, die nötig wären, um das Benutzererlebnis für das angedachte Feature so perfekt wie möglich zu machen.

Der Status, das unbekannte Wesen

Nicht direkt zum Status gehören aber abgeleitete Informatio­nen, die aus dem aktuellen Kontext stammen. Wenn der Benutzer eingeloggt ist, weiß das System automatisch, welche Sprache er spricht, in welcher Währung er bezahlen wird und ob er als Endkunde Bruttopreise oder als gewerblicher Kunde Nettopreise angezeigt bekommt.
Rein technisch zählt nur die Benutzeranmeldung zum Status, da die anderen Informationen ja aus der tagesaktuellen Benutzerdatenbank stammen. Ob im Hinblick auf unser Benutzererlebnis die Credentials dabei ebenfalls gespeichert werden sollten (natürlich verschlüsselt), oder doch besser nicht, hängt dabei von anderen Faktoren ab. Es spielt für unsere Betrachtung aber auch gar keine Rolle.
Was für eine Desktop-Anwendung bereits eine durchaus interessante Übung ist, wird bei einer mobilen oder Webapplikation noch komplexer. Hier kommt nämlich noch die Frage hinzu, wo diese flüchtigen Informationen über den internen Zustand gespeichert werden. Lebt der Status rein auf der Seite des Clients? Oder gibt es auch Daten, die auf dem Server innerhalb einer Session verwaltet werden?

Kein Patentrezept

Leider gibt es dafür kein Patentrezept. In der Praxis gilt es, einen für den konkreten Anwendungsfall sinnvollen Kompromiss zu finden. Das kann durchaus auch bedeuten, dass Daten rein aus Performancegründen redundant sowohl auf dem Server als auch auf dem Client vorgehalten werden. In bestimmten Fällen ist es aus Sicht einer optimalen Benutzererfahrung sinnvoll, die zusätzliche Komplexität in Kauf zu nehmen, die mit redundanter Datenhaltung unweigerlich einhergeht.
Aufhänger für den Artikel ist die Bedeutung des Status im Kontext der Applika­-
tion, insbesondere der Bedienoberfläche. Das scheint zunächst willkürlich. Hält man sich allerdings vor Augen, dass die Oberfläche eines Webservice im Grunde nichts anderes als die von diesem offengelegte Programmierschnittstelle (API) ist, wird klar, dass eine grafische Bedienoberfläche lediglich eine mögliche Repräsentation einer Interaktionsschnittstelle ist. Schließlich spräche nichts dagegen, unserer Anwendung auch eine Kommandozeilenschnittstelle oder ein REST-Interface zu verpassen.
Beschränken wir uns also im Folgenden auf den Status eines GUI. Fürs Erste klicken wir einfach nur ein wenig in unserer hypothetischen Applikation oder Webseite herum (Bild 1).
Struktur: Eine hypothetische Applikation (Bild 1)
Während wir das tun, beobachten wir, wie sich aufgrund unserer Interaktion der Zustand ändert. Nach einer Weile stellt sich uns die Frage, woher die Applikation eigentlich weiß, welche Bereiche des UI aufgrund der vorgenommenen Änderungen am inneren Zustand aktualisiert werden müssen? Wieso weiß die Anwendung, dass beim Anklicken eines Eintrags in der Liste die Detailansicht aktualisiert werden muss?
Wer schon das eine oder andere komplexere Formular, ob Web oder Desktop, gebaut hat, der kann davon ein Lied singen. Je mehr Controls und Logik hinter einem UI stecken, desto wichtiger ist eine präzise Planung vor der Implementierung. In der Praxis neigen auch anfänglich kleine und überschaubare Anforderungen dazu, im Lauf der Zeit immer umfangreicher zu werden.
Mit dem Effekt, dass auch aus einem anfangs scheinbar simplen Formular ein komplexes Monster werden kann. Spätestens wenn ein einziger Mausklick wahre Änderungskaskaden auslöst, die sich im UI durch hektisches, sekundenlanges Flackern unzähliger Komponenten widerspiegeln, wissen wir, dass dringend ein Redesign fällig ist (Bild 2).
Komponenten: Eine mögliche Aufteilung in Komponenten (Bild 2)
Angesichts derartiger Probleme und der damit im Quelltext meist einhergehenden Komplexität der Marke gewachsenes System wundert es nicht, dass man sich schon vor Längerem Gedanken über effizientere und besser wartbare Lösungen gemacht hat. Im Lauf der Zeit sind dazu ganz verschiedene Ansätze entstanden.
Die Entwicklung ist aber keineswegs abgeschlossen, auch heute werden bestehende Verfahren verbessert und neue Verfahren aus gewonnenen Erfahrungen und frischen Ideen geboren.
Ihren Siegeszug traten die ersten grafische Oberflächen vor allem auch dank der Objektorientierung an. Ein Mantra der klassischen Objektorientierung ist die Einheit von Daten und Verhalten, die in logischen Einheiten namens Klassen zu kapseln seien. Um die Daten zu manipulieren, werden je nach Lesart und Benennungskonvention entweder Nachrichten an Instanzen der Klasse gesendet, oder aber einfach deren Methoden aufgerufen. Entscheidend ist hier, dass die Daten von außen nicht direkt manipuliert werden können.
Also reicht es demnach, einfach die Daten sinnvoll zu strukturieren und dann jede logische Einheit mit Methoden zu versehen? Dass dieser naive Ansatz in der realen Welt wohl doch nicht die letzte Wahrheit sein kann, bemerkt man schnell. Studiert man die bekannten Patterns der Gang of Four, die als praxisrelevante Sammlung gängiger Best Practices auch heute noch brandaktuell sind, wird klar, dass wohl mehr dazugehört.
Ein gut aufgebautes System zeichnet sich vor allem durch eine wartungsfreundliche und erweiterungstolerante Struktur aus. Bewährte Mittel sind hier vor allem die Trennung von Verantwortlichkeiten (das bekannte Single Responsibility Principle) und eine sinnvolle und strikte Kapselung. Geringe und wohldefinierte Abhängigkeiten sorgen nicht nur für eine stressfreie Integration eines Modules, einer Bibliothek oder einer Klasse. Sie sorgen auch dafür, dass diese Module isoliert getestet werden können. Als Bonus bekommt man sodann den heiligen Gral der OOP überreicht: Wiederverwendbarkeit.

OOP nur simuliert

Wenn hier von Verantwortlichkeiten die Rede ist, so bezieht sich das nicht nur auf Daten. Gemeint ist vor allem auch die Logik zur Arbeit mit den Daten. Natürlich kann man das Rendering, die Serialisierung in drei Datenformaten, Ausgabeformatierung und fachliche Berechnungen alle zusammen mit den dazugehörigen Daten in eine Klasse packen und sich freuen, wie furchtbar objektorientiert das alles ist. Die schlechte Nachricht? Ist es nicht. Denn hier wird OOP nur simuliert.
Wer kennt sie nicht, diese Formulare, die nicht nur Con­trols beherbergen, sondern auch jede Menge Logik, um diese anzusteuern? Und natürlich die diversen Statusvariablen, um den Dialog zu steuern. Eine Dialogklasse, sie alle zu regieren … ein klassisches Brownfield eben, das alle nur denkbaren Nachteile in einer Klasse vereint. Weder Status noch Verhalten sind getrennt testbar. Schlimmer noch, die Wiederverwendung der Formularlogik in einer webbasierten Lösung oder einer Mobil-App ist durch das enge Verweben der drei Aspekte faktisch unmöglich. Da der Code außerdem auch noch eng an die jeweiligen plattformspezifischen Controls gebunden ist, weil es keine Abstraktionschicht gibt, ist auch nachträgliches Refactoring mindestens arbeitsaufwendig, wenn nicht schon von vornherein zum Scheitern verurteilt.
OOP heißt eben gerade nicht, dass man einfach Daten und Funktionen zusammenpappt und fertig ist die objektorientierte Laube. Sicher, bei kleinen, überschaubaren Formularen und Anwendungen funktioniert das prima. Unglücklicherweise skaliert das so angeeignete Wissen des Entwicklers aber nicht, sodass komplexere Architekturen gelegentlich trotz vermeintlicher OOP-Expertise dann doch als chaotischer, unwartbarer Klassenverhau enden.
An dieser Stelle lassen wir die Grundsatzdiskussion, ob OOP nun tot sei oder nicht, einfach links liegen und wenden uns den Erkenntnissen zu, die wir mitnehmen: Offensichtlich ist es also eine clevere Idee, die Bestandteile einer Oberfläche in mindestens drei Verantwortungsbereiche zu trennen. Als Erstes hätten wir den recht offensichtlichen Teil der sichtbaren Controls und sonstigen UI-Elemente.
Außerdem braucht es irgendeine Form von Logik, die für die Steuerung der Abläufe im Dialog, für Datenvalidierung und eventuell nötige Formatierung sowie für die Aufbereitung von Ein- und Ausgabedaten zuständig ist. Idealerweise sind diese Dienstleister selbst komplett statuslos und leichgewichtig, sodass Erzeugung und Zerstörung jederzeit nach Bedarf erfolgen können. Drittens gibt es natürlich Daten, die sich wiederum in die schon bekannten Gruppen »persistente Anwendungsdaten« und »flüchtiger Programmstatus« aufteilen lassen. Irgendwie müssen wir diese Bausteine nun noch organisieren und miteinander verknüpfen.
Und tatsächlich halten sich nahezu alle heute gängigen Verfahren an dieses grundlegende Schema. Die Begriffe und Aufgaben mögen im konkreten Fall abweichen und Verantwortlichkeiten geringfügig anders strukturiert sein, dennoch läuft letztlich alles auf dieses zugrunde liegende Modell hi­naus.

Bäume aus Komponenten

An dieser Stelle noch eine kurze Bemerkung zu den folgenden Beispielen. Es ist natürlich vollkommen klar, dass diese lediglich ein Ausschnitt aus einer Momentaufnahme des aktuellen Status quo der UI-Entwicklung sein können, insbesondere auch ohne jeden Anspruch auf Vollständigkeit. Sie sind vielmehr als Illustration und Anregung zu verstehen, deswegen gehen wir auch absichtlich nicht so sehr auf Implementationsdetails ein. Der gesamte Bereich ist nach wie vor und gerade im Web in heftiger Bewegung, sodass die einzige Konstante tatsächlich die Veränderung ist.
Grafische Oberflächen sind normalerweise so organisiert, dass sich daraus eine baumartige Struktur ergibt. Controls befinden sich in Dialogen oder Anwendungsfenstern und diese wiederum auf einem Desktop mit grafischen Eigenschaften.
Dasselbe wiederholt sich in kleinerem Maßstab innerhalb eines Dialogs, Formulars oder einer Webseite, wo Controls und grafische Elemente in andere eingebettet werden, um eine für den konkreten Anwendungsfall optimal geeignete Struktur zu erzeugen.

Logische Abhängigkeiten

Zu unterscheiden sind hierbei zwei Strukturen. Zum einen die gerade erwähnte visuelle, die sich durch die Schachtelung der UI-Elemente ergibt. Zum anderen gibt es aber auch logische Abhängigkeiten aufgrund der Daten, die diese UI-Elemente repräsentieren. Für gewöhnlich sind beide Aspekte komplett getrennt.
Die bloße Anwesenheit eines bestimmten Datenelements kann dafür sorgen, dass Menüpunkte und Toolbar-Buttons aktiviert werden und sich eine geöffnete Detailansicht mit Daten füllt. Sind in dem Datenelement bestimmte Werte vorhanden, werden in der Oberfläche weitere Daten angezeigt und zusätzliche Optionen und Querverweise sichtbar.
In typischen Webapplikationen sind daher die beiden Belange Layout und dahinterstehende Logik auch sauber getrennt in CSS und JavaScript, das umgebende statische ­HTML dient oftmals nur noch als Container, der alles zusammenhält. Analoges findet sich bei Oberflächen, die mit XAML gebaut werden. Was den Layouter freut, ist auch für den Entwickler eine tolle Sache. Effektiv kann damit bei geschicktem Aufbau das komplette sichtbare UI wegabstrahiert werden.
Die Zielstellung ist wieder dieselbe wie oben: Wir möchten eine Trennung von Verantwortlichkeiten, sinnvolle und strikte Kapselung und möglichst geringe, wohldefinierte Abhängigkeiten haben.
Insbesondere das Styling der Oberfläche sollte als separate Verantwortlichkeit unabhängig von der dahinterstehenden Logik sein. Denn letztlich sind sowohl Kunden als auch Anwender an einem konsistenten UI interessiert. Gibt das eine Komponente nicht her, wird sie optisch immer als Fremdkörper auffallen.
Eine wesentliche Aufgabe der Schicht hinter den sichtbaren UI-Komponenten ist es demnach, eine layoutfreundliche Abstraktion der zugrunde liegenden Daten bereitzustellen.

Model-View-Konzepte

Der Namensgeber der Gruppe, das Model-View-Controller-Konzept, geht auf Smalltalk-80 zurück. Wie aus dem Namen unschwer herzuleiten ist, gibt es in diesem Modell drei Bestandteile, die miteinander interagieren. Dabei gelten ein paar einschränkende Regeln.
Im klassischen MVC reagiert der Controller auf eingehende Informationen. Diese stammen einerseits vom Model, das bei Datenänderungen Events sendet. Die zweite Quelle für eingehende Ereignisse sind die Interaktionen des Users mit der Anwendung. Der Controller ist verantwortlich für die Aktualisierung von View und Model. Die View kann ihrerseits ebenfalls Ereignisse des Models abonnieren (Bild 3).
Model-View-Controller: Schematische Darstellung des MVC-Modells (Bild 3)
Nachteilig an MVC ist, dass auch insbesondere die View noch Logik enthalten kann und wird. Damit ist de facto immer noch eine zu enge Kopplung gegeben. Aus diesem Grund wurde MVC zu Model-View-Presenter weiterentwickelt.
Hauptmerkmal ist hier, dass die gesamte View-Logik mit in den Controller wandert, der dadurch zum Presenter mutiert. Wie vorher schon der Controller, so empfängt auch der Presenter Ereignisse von Model und View und aktualisiert im Gegenzug Model und View. Die View selbst ist jetzt ein rein passives Element, das nur noch die vom Presenter vorformatierten Daten anzeigt und Benutzereingaben entgegennimmt. Als weitere Einschränkung kommt hinzu, dass die View keine Verbindung mehr zum Model haben darf, alle Kommunikation verläuft über den Presenter (Bild 4).
Model-View-Presenter: MVC wurde zu Model-View-Presenter weiterentwickelt (Bild 4)
Mit dem Ziel, die so gekapselte Logik erstens testbar zu machen und zweitens noch weiter zu abstrahieren und zu standardisieren, sodass sich faktisch beliebige Views an dieselbe Darstellungslogik anschließen lassen, wurde mit Model-View-Viewmodel eine weitere Variante ins Leben gerufen (Bild 5).
Model-View-Viewmodel: Eine weitere Variante des MVC-Modells (Bild 5)
Besonders auf XAML-basierten Plattformen hat sich dieser Ansatz als De-facto-Standard durchgesetzt. Kernpunkte des Konzepts sind wieder drei miteinander interagierende Komponenten. Das Viewmodel enthält dabei die Darstellungs­logik inklusive Formatierung und Datenkonvertierung. Die Daten werden den View-Komponenten über standardisierte Schnittstellen angeboten und deklarativ über Data Binding angeschlossen. Das Viewmodel verfügt außerdem über eine generische Command-Schnittstelle, die das Auslösen und die Abfrage der Verfügbarkeit von Kommandos ermöglicht. Auch bei MVVM haben View und Model keine direkte Verbindung miteinander.
Ein wesentliches Konzept bei MVVM ist, dass Viewmodels analog zu UI-Komponenten auch geschachtelt eingesetzt werden. Üblicherweise ist ein Viewmodel auch genau für einen Bereich im UI zuständig, sodass die Viewmodels letztlich eine Hierarchie bilden, die den komponentenseitigen Dialog­aufbau mehr oder weniger präzise widerspiegelt. Mit MVVM bekommen wir also endlich auch kleine, wohldefinierte Verantwortungsbereiche im UI (Bild 6).
HMVC steht für Hierarchisches Model-View-Controller (Bild 6)
Genau diese kleinteiligen Verantwortungsbereiche verspricht auch HMVC, was für Hierarchisches Model-View-Controller steht. Die Idee hierbei ist, die Struktur der Oberfläche nicht in zwei unabhängigen Bäumen aus Controls und Viewmodels aufzubauen, sondern eine komplette, hierarchische Struktur aus lauter kleinen MVC-Bausteinen zu erzeugen. Die Verbindung der Einzelteile untereinander fällt in den Aufgabenbereich der Controller. Im Gegensatz zu ­MVVM ist die Oberflächenlogik nicht wirklich gut isolier- und testbar, allerdings eröffnet sich die Möglichkeit, einzelne Bausteine zu entnehmen und diese isoliert zu testen.

Command Query Responsibility Segregation

Wir haben uns jetzt eine ganze Weile hauptsächlich mit den oberflächenrelevanten Teilen der Konzepte beschäftigt. Die Frage, wie die Zustandsdaten im Backend manipuliert und abgefragt werden, ist aber mindestens genauso interessant.
Um mit den steigenden Anforderungen an Skalierbarkeit Schritt halten zu können, wurde von Bertrand Meyer ein Konzept namens Command Query Separation (CQS) entwickelt, aus dem wenig später Command Query Responsibility Segregation (CQRS) wurde (Bild 7).
Command Query Separation (CQS): Mit diesem Konzept wollte man mit den steigenden Anforderungen an Skalierbarkeit Schritt halten (Bild 7)
Die Idee hinter beiden ist, dass man reine Datenabfragen von sogenannten Kommandos trennen kann, die tatsächlich Daten manipulieren. CQRS ist daher besonders für Anwendungen geeignet, in denen Lesezugriffe überwiegen. Der Trick hierbei ist, dass reine Lesezugriffe den internen Zustand nicht ändern.
Sie sind komplett frei von Seiteneffekten und lassen sich regelmäßig sehr gut cachen. Solange die zugrunde liegenden Ausgangsdaten unverändert bleiben, kann jede weitere Leseanfrage äußerst effizient bedient werden.
Der Gegenpart zu den rein lesenden Abfragen sind die Kommandos. Das Ändern des Zustands ist ausschließlich durch den Aufruf eines solchen Kommandos möglich. Es liefert nach der reinen Lehre auch keine Rückgabedaten, allerdings ist in der Praxis häufig zumindest eine Quittierung der Abarbeitung erwünscht. Und obwohl das eigentliche CQRS-Pattern im Kern recht simpel daherkommt, ermöglicht die strikte Trennung der Kommandos eine Vielzahl von Wegen zur Implementierung. Die Möglichkeiten reichen hierbei von simpler, synchroner Abarbeitung als Transaktion bis zur verteilten, asynchronen Kommandoausführung via Messagebus oder Eventsourcing. Last, not least ist CQRS als generisches Pattern nicht beschränkt auf Server beziehungsweise Backend, sondern kann ebenso auch auf der Clientseite eingesetzt werden.
Nach unserem kurzen Exkurs zur Verwaltung des serverseitigen Status geht es nun wieder zurück an die Oberfläche. Anno 2013 entwickelte Facebook für eigene Zwecke ein Framework namens React, das einen durchaus interessanten Ansatz verfolgt.
Bei React werden nämlich Skriptcode und HTML scheinbar wieder eng miteinander vermischt. Tatsächlich trügt der Schein jedoch, denn das, was auf den ersten Blick aussieht wie in JavaScript eingebettetes HTML, ist nur eine HTML-ähnliche Syntax, die einen Knoten im HTML-DOM beschreibt. Und eigentlich handelt es sich auch gar nicht um normales JavaScript, sondern um JSX.

Strukturen im HTML-DOM

Diese HTML-ähnliche Beschreibung des gewünschten DOM-Teilbaums kann auch Variablen beinhalten, die bei Verwendung innerhalb des Pseudo-HTML in geschweifte Klammern gesetzt werden müssen. React rendert aus diesen Informatio­nen die finalen Strukturen im HTML-DOM selbst, sodass das Ganze plattformunabhängig wird. Und tatsächlich beherrscht React sowohl serverseitiges als auch clientseitiges Rendering. Im folgenden Listing wird ebenfalls deutlich, wie das Konzept hinter ­React funktioniert. Jede React-Klasse muss mindestens eine render()-Funktion definieren:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="build/react.js"></script>
    <script src="build/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      ReactDOM.render(
        <h1>Hello, world!</h1>,
        document.getElementById('example')
      );
    </script>
  </body>
</html>
Um interne Zustandsdaten zu speichern, sollte darüber hinaus auch eine Funktion getInitialState() bereitgestellt werden. Dieser Status ist klassenintern dann einfach über this.state abrufbar. Analog wird auf optional in das Element per Attribut hineingereichte Daten über this.props zugegriffen. Um den Status zu ändern, ruft man die eingebaute React-Funktion setState() auf und übergibt dieser einen kompletten oder auch nur partiellen neuen Status. React wird sodann die aktuellen Daten mit den neuen mischen und anschließend dafür sorgen, dass die render()-Funktion aller von Änderungen des Status betroffenen Elemente aufgerufen wird. Das bedeutet nichts anderes, als dass alle betroffenen Teile des DOM neu gerendert werden. Da React aber darauf optimiert ist, tatsächlich nur die geänderten Teile neu zu rendern, geht das vergleichsweise schnell und funktioniert umso besser, je besser sich der Status und das UI in getrennte logische Einheiten aufteilen lassen. Reicht man hingegen immer den kompletten Status durch alle Elemente durch, wird das Konstrukt schnell ineffizient:
var World = React.createClass({
  getInitialState: function() {
    return {clicks: 0};
  },

  render: function() {
    return (
      <div className="world" onClick={this.clicked}>
        Hello from {this.props.name}!<br />
        We had {this.state.clicks} visitors so far.
      </div>
    );
  },
 
  clicked: function() {
    this.setState({
      clicks: this.state.clicks + 1
    });
  }
 
});


var MultiHello = React.createClass({
  render: function() {
    return (
      <div className="hello">
        <World name="Earth" />
        <World name="Pluto" />
        <World name="Kepler-452b" />
      </div>
    );
  }
});

ReactDOM.render(
  <MultiHello />,
  document.getElementById('content')
);
So schön das optimierte Rendern bei React funktioniert, so nachteilig ist es auch, dass der interne Zustand viel zu eng mit dem UI verflochten ist. Nicht nur das, darüber hinaus verteilt sich der Status auch noch auf viele kleine Klassen und Ereignis-Handler. Leider tendieren solche Konstruktionen nicht nur dazu, mit zunehmender Größe wartungsunfreundlicher zu werden. Auch die Performance leidet, je mehr reaktive Elemente auf der Seite existieren. Überdies kommt auch noch die potenzielle Gefahr möglicher Inkonsistenzen hinzu, die durch redundante Datenhaltung entstehen kann.
Auch die Entwickler bei Facebook standen irgendwann mit React vor genau diesen Problemen. Um die bröckelnde Architektur wieder in den Griff zu bekommen, entstand als Weiterentwicklung Flux. Kernstück von Flux ist der Aufbau der Elemente in Form einer Pipeline aus Actions, Dispatcher, Store und View (Bild 8).
Flux: Aufbau der Elemente in Form einer Pipeline aus Actions, Dispatcher, Store und View (Bild 8)
Die Statusdaten werden bei Flux nicht mehr in den einzelnen Elementen verstreut vorgehalten, sondern wandern in sogenannte Stores. Damit die Sache auch interaktiv wird, dürfen die Views als einzige Elemente Aktionen erzeugen und in die Pipeline einspeisen. Aktionen sind nichts anderes als Ereignisse. Es sind einfach Nachrichten, die die dahinterliegenden Komponenten darüber informieren, dass sich etwas Bestimmtes am Zustand ändert.
Eine solche Aktion könnte also lauten: »Benutzer hat seine Anschrift geändert« oder »Artikel A mit der Menge B wurde in den Warenkorb gelegt«. Diese Aktionen werden vom Dispatcher an die Stores verteilt. Je nach Anforderungen gibt es meist einen, alternativ auch mehrere Stores pro Applikation. Der oder die Stores sind dafür verantwortlich, den aktuellen Zustand vorzuhalten und zu managen. Abhängig von der eintreffenden Aktion wird der bestehende Status mittels sogenannter Reducer-Funktionen mutiert. Letztere sind im Grunde nichts anderes als spezialisierte Ereignis-Handler. Da die Aktionen streng serialisiert eintreffen, lässt sich der Zustand der Applikation zu jedem Zeitpunkt jederzeit aus dem bekannten Anfangszustand und dem Strom an eintreffenden Aktionen herleiten. Das kann für den Entwickler extrem hilfreich sein, wenn es einen schwer reproduzierbaren Bug zu finden gilt:
var INITIAL_STATE = {
 value: 0,
 evtCount: {
  add: 0,
  set: 0
 }
}
var reducer = function(state, action) {
 if(state === undefined) {
  return INITIAL_STATE;
 }
 var newState = state;
 switch(action.type) {
  case 'add_counter':
   var newEventCount = Object.assign( {},
    state.evtCount,
    {
     add: state.evtCount.add + 1
    });
   var newState = Object.assign( {},
    state,
    {
     value: state.value + action.delta,
     evtCount: newEventCount
    });
   break;

   case 'set_counter_value':
   var newEventCount = Object.assign( {},
    state.evtCount,
    {
     set: state.evtCount.set + 1
    });
   var newState = Object.assign( {},
    state,
    {
     value: action.value,
     evtCount: newEventCount
    });
   break;
 }

 return newState;
}
Als logische Weiterentwicklung von Flux kann Redux gelten. Genau genommen handelt es sich bei Redux um eine komplett unabhängige Entwicklung, die aber inzwischen als das bessere Flux gehandelt wird. Die aktuelle Version von Redux setzt voll auf ECMAScript 2015 und benötigt eine entsprechend barocke, auf Node.js aufbauende Infrastruktur. Bereits für das einfachste Tutorial-Beispiel schaufelt Node.js weit über 90.000 Dateien auf die Platte. Wie React und Flux lässt sich Redux auch ohne Node.js einsetzen, indem man die an sich recht schlanke JavaScript-Bibliothek manuell einbindet. Allerdings verliert man beim Verzicht auf Node.js auch die Möglichkeit, JSX und ECMAScript 2015 serverseitig zu kompilieren.
Die wichtigste Änderung bei Redux gegenüber Flux ist, dass der Status konsequent als unveränderlich oder immutable angesehen wird und konsequent als Single Immutable State Tree bezeichnet wird. Die logische Folge ist, dass die reduce()-Funktionen in Redux nicht mehr den bestehenden Status manipulieren dürfen, sondern wirklich jedes Mal ein neues Statusobjekt für die betroffenen Teilbäume mit den gewünschten Anpassungen erstellen müssen. Diese Einschränkungen haben allerdings zur Folge, dass die Änderungs­erkennung in Redux extrem performant ist, da effektiv nur noch Objekt­instanzen verglichen werden müssen

Die finale Lösung?

Ist das nun die finale Lösung aller Probleme? Das sprichwörtliche Ende der Fahnenstange? Keineswegs. Wie mit allen anderen Ansätzen bleiben auch mit Redux ausreichend Probleme übrig. Eines davon betrifft eben genau jenen Single Immutable State Tree, der als einziges Element den Zustand der Applikation verwalten soll. Was in der Theorie großartig klingt, treibt in der Praxis gelegentlich merkwürdige Blüten. der Verzicht auf lokale Statusdaten in den Komponenten bedeutet im Umkehrschluss, dass auch jedes noch so unwichtige temporäre Datenfitzelchen in ebenjenem gemeinsamen Status Tree abgelegt werden muss.
Das aber ist nicht wirklich zielführend, sodass man in der Praxis dann eben doch wieder wie in React zum lokalen Status greift. Das Problem an der Sache ist allerdings, dass die Aufweichung des Gebots »Du sollst deinen Status stets im Single Immutable State Tree speichern« gemäß der Broken-Window-Theorie die Tür öffnet für ungewollte Abweichungen von der reinen Lehre und für Probleme, die redundante Datenhaltung mit sich bringt. Ein sinnvoller Kompromiss könnte daher sein, dass man vereinbart, solche rein temporären Daten in einem Seitenast des Statusbaums abzulegen.

Fazit

Die Struktur der Daten, die den internen Status abbilden, ist essenziell für jede Anwendung und die Basis für Architektur und Applikationsdesign. Zugleich müssen wir akzeptieren, dass sich die Anforderungen an unsere Applikation im Lauf der Zeit ändern. All diese Änderungen betreffen in irgendeiner Weise die Daten, mit denen die Applikation umgeht. Selbst rein algorithmische Änderungen haben meist Auswirkungen auf das UI und damit wieder auf die zugrunde liegenden Zustandsdaten der Anwendung. Änderungen an den Datenstrukturen sind schwer, aber früher oder später unvermeidlich. Aufgabe von Entwicklern und Software-Architekten ist es, optimale Voraussetzungen zu schaffen, damit die Applikation mit dieser Entwicklung Schritt halten kann.

ECMAScript 2015 und Babel

Seit Mitte 2015 ist das neue ECMAScript 2015 beschlossene Sache. Die Browserhersteller haben diesen mittlerweile acuh mehr oder weniger umgesetzt, bei einigen marktgängigen Browsern aber bislang eher sparsam. Trotzdem muß man nicht auf den neuen, gelegentlich auch als ECMAScript 6 bezeichneten, Standard nicht verzichten. Dank Babel ist es möglich, die fortschrittliche Sprachvariante auch heute schon zu benutzen. Hinter den Kulissen übersetzt Babel die neuen Konstrukte in normales ECMAScript 5. welches aktuele Browser in der regel problemlos ausführen können.

Polyfill - Spachtelmasse für den Browser

Der Begriff Polyfill ist eine Erfindung von Remy Sharp. Im britischen Sprachraum ist Polyfilla ein bekanntes Produkt zum Ausbessern, Füllen und Glätten von Wänden. Als Analogon zur physischen Spachtelmasse ist ein Polyfill für den Webentwickler einfach ein Stück JavaScript, mit dem sich fehlende Funktionen emulieren lassen.

JSX - Syntaxerweiterungen für JavaScript

Das unter anderem von React verwendete JSX ist eine XML-artige Syntaxerweiterung für JavaScript. Zweck der Übung ist es, mittels der bekannten XML-Syntax hierarchisch gegliederte Strukturen zu beschreiben, die letztlich in HTML-DOM-Konstrukte übersetzt werden. JSX ist an die verworfene EX4-Spezification der ECMA angelehnt. JSX wurde von Facebook 2014 unter einer Creative Commons - Lizenz veröffentlicht.

Broken Windows Theorie

Der Begriff der Broken Windows Theorie wurde von den Sozialwissenschaftlern James Wilson und George Kelling geprägt. Er geht auf ein Experiment des Psychologen Philip Zimbardo von 1969 zurück. Im Wesentlichen besagt die Theorie, daß eine sichtbare Beschädigung, im Experiment war dies eine eingeschlagene Fensterscheibe an einem PKW, die Hemmschwelle zu weiterer Zerstörung raide absenkt. Das Umfeld geht davon aus, daß das Objekt herrenlos ist und sich offensichtlich niemand mehr um dessen Erhaltung kümmert. Besagter Philip Zimbardo studierte gemeinsam mit Stanley Milgram und zeichnete auch für das bekannte Stanford Prison Experiment verantwortlich.
Dokumente
Diesen Artikel als PDF lesen.



Das könnte Sie auch interessieren