Reaktive Programmierung
Reaktive Programmierung ist ein weiteres Programmier-Paradigma, ähnlich wie objektorientierte oder prozedurale Programmierung. Sie ist ziemlich eng mit der funktionalen Programmierung verwandt. Bei der reaktiven Programmierung geht es darum, Daten – und vor allem Datenströme – geschickt durch die Anwendung zu leiten und im Verlauf zu transformieren. So können Variablen auf anderen Variablen basieren und verändern sich so automatisch, wenn sich eine ihrer Abhängigkeiten ändert. Das passiert ganz von alleine und macht das manuelle Ändern von Daten überflüssig. Besonders nützlich ist dies für sich häufig ändernde und asynchrone Daten. Deshalb ist reaktive Programmierung vor allem im Frontend beliebt, aber auch im Backend kann sie eine echte Bereicherung sein – zum Beispiel bei der Verarbeitung von Sensordaten oder bei Proxyverbindungen.
Was sind überhaupt Datenströme?
Das lässt sich am besten mit einem Beispiel aus der Praxis erklären:
Stell dir vor, du baust eine Fabrik mit einer Fertigungsstraße. Wie in den Computerspielen Factorio oder Satisfactory. Dort beginnt alles damit, dass Rohmaterialien für die Produktion angeliefert werden. In der reaktiven Programmierung ist das zum Beispiel ein User-Event wie ein Klick, ein API-Call oder eine Verbindung.
Dann werden die Rohmaterialien weiterverarbeitet. Auch die Daten können bearbeitet werden, zum Beispiel durch den map-Operator, der die Rohdaten nimmt, verändert und dann weitergibt. (Achtung: map ist ähnlich zur Listenoperation map, funktioniert hier aber mit asynchronen Daten und nicht mit Listen-Elementen.)
Als Nächstes könnten die bearbeiteten Rohmaterialien miteinander kombiniert werden, also zum Beispiel das Zusammenschrauben von zwei Teilen. Bei den Datenströmen kann man sich das so vorstellen, dass zwei oder mehr Datenströme genommen und kombiniert werden. Die neuen, kombinierten Daten werden dann in einem neuen Datenstrom weitergegeben.
Am Ende der Produktionsstraße gibt es dann vielleicht noch eine Qualitätskontrolle, bei der defekte Teile aussortiert werden. Auch in Datenströmen können Daten gefiltert werden, zum Beispiel über den filter-Operator.
Schließlich werden die fertigen Produkte ausgeliefert. Mit reaktiven Datenströmen kann das auf verschiedene Weisen geschehen. Einige Frameworks wie Angular können die Daten sofort im Template anzeigen. Ansonsten kann man sich auch auf die Datenströme abonnieren und auf jeden neuen Wert reagieren.
Ein konkretes Beispiel
Schauen wir uns das Ganze mal an einem konkreten Beispiel an. Hierbei wird TypeScript mit RxJS, einer Bibliothek für reaktive Programmierung in JavaScript, verwendet. Bei weiterführenden Fragen zu den verwendeten Operatoren findest du genaue Erklärungen in der Dokumentation unter RxJS API.
Das folgende Beispiel simuliert eine Webseite, auf der eine Liste von Blog-Artikeln angezeigt werden soll. Über ein Suchfeld kann der Nutzer Suchbegriffe eingeben. Die Titel der Artikel sollen dann nach diesem Begriff gefiltert und die verbleibenden Artikel angezeigt werden.
Der TypeScript-Code ist in Klassen unterteilt. In beiden Codebeispielen gibt es ein Interface für einen Artikel sowie eine Klasse namens ApiService, die über eine entsprechende Funktion Artikel bereitstellt. Zusätzlich gibt es die Klasse UI, die über eine Methode die Darstellung simulieren soll.
Das Setup sieht wie folgt aus:
Imperative Variante
Zuerst implementieren wir die Artikel-Abfrage in einer rein imperativen Variante.
Zu Beginn der Klasse werden alle später benötigten Attribute deklariert (Zeile 2-8). Dabei wird searchField (Zeile 5) direkt mit dem entsprechenden Wert befüllt. Im Konstruktor wird dann die asynchrone Service-Methode getArticles aufgerufen, die ein Promise zurückliefert (Zeile 11). Sobald dieses Promise gelöst ist, wird der Wert manuell in das articles-Attribut geschrieben (Zeile 12). Zusätzlich wird eine Liste gefilterter Artikel berechnet und in das Attribut filteredArticles geschrieben (Zeile 13-16). Zuletzt wird im Konstruktor ein Eventhandler registriert (Zeile 20), der bei Änderungen des Suchbegriffs eine Funktion aufruft, welche dafür verantwortlich ist, die Variable filteredArticles neu zu setzen (Zeile 23-27).
Probleme bei dieser Art von Code sind, dass von mehreren Stellen aus dem Code auf die Variablen zugegriffen wird. Das Attribut filteredArticles wird sowohl im Konstruktor nach der Auflösung des Promises als auch in der Funktion, die der Eventhandler aufruft, gesetzt. Ebenso muss die displayUi-Funktion an beiden Stellen aufgerufen werden. Dies widerspricht dem DRY-Prinzip (Don’t Repeat Yourself), das darauf abzielt, Redundanzen im Code zu vermeiden. Verändert sich nun etwas an der Applikation, müssen Anpassungen an all diesen Stellen vorgenommen werden. Dabei ist es essenziell, keine Stelle zu vergessen, was durch die Verteilung über die gesamte Codebasis nicht immer einfach ist. Bei einer kleinen Anwendung mag das noch gehen, aber in einer Größeren wird es deutlich aufwändiger. Des Weiteren wird der Fluss der Anwendung in diesem Beispiel nicht besonders deutlich, unter anderem wegen der Registrierung eines separaten Eventhandlers, durch den der Code auseinandergezogen wird.
Reaktive Variante
Implementieren wir nun die reaktive Variante, um die Unterschiede deutlich zu machen.
Bei der reaktiven Variante fällt auf, dass die Klasse weder einen Konstruktor noch andere Methoden hat. Die gesamte Logik, wie sich Variablen ändern können, wird in den Attribut-Deklarationen beschrieben. Diese sind readonly und können somit von außen nicht verändert werden. Die einzelnen Deklarationen beschreiben genau, welchen Status die entsprechenden Observables unter welchen Umständen annehmen. searchTerm (Zeile 6-9) entspricht immer dem String, der im Input-Feld steht. Articles (Zeile 11) enthält ein Observable, das die Antwort der API-Anfrage enthält, sobald das Promise aufgelöst ist. filteredArticles (Zeile 13-25) ist eine Kombination aus dem neuesten Wort aus dem Input und den Artikeln. Wenn die Artikel geladen sind oder jedes Mal, wenn sich der Suchbegriff ändert, wird die Liste der Artikel nach dem Begriff gefiltert und die neue Liste ausgegeben. Diese wird dann dargestellt (Zeile 26).
Wie zu erkennen ist, ist die reaktive Variante deutlich lesbarer. Der Code passt sich dem Ereignisfluss der Anwendung besser an. Code, der zusammengehört, steht zusammen. Zudem erlaubt diese Art des Codes einen deutlich einfacheren Umbau, Erweiterung oder Refactoring, da nur an einer Stelle gesucht werden muss – bei der Deklaration – womit das DRY-Prinzip eingehalten wurde.
Dieser Sachverhalt wird noch deutlicher, wenn man eine schematische Darstellung des Codes betrachtet. Diese zeigt die verschiedenen Teile des Codes und wie sie aufeinander zugreifen. Das erste Bild veranschaulicht die Zusammenhänge des imperativen Codes, während das zweite Bild die Zusammenhänge des deklarativen Codes darstellt. Hierbei sind Code-Abschnitte mit ähnlichen Aufgaben in der gleichen Farbe dargestellt. Die Abhängigkeiten, also ein Funktionsaufruf oder das Lesen und Setzten einer Variable, sind mit Pfeilen dargestellt.
Schematische Darstellung des imperativen Codes
Schematische Darstellung des deklarativen Codes
Fazit
Reaktive Programmierung bietet eine moderne und elegante Möglichkeit, mit sich ändernden und asynchronen Daten umzugehen. Sie kann den Code klarer und wartbarer machen, indem sie Variablen automatisch aktualisiert, wenn sich deren Abhängigkeiten ändern. Besonders in der Frontend-Entwicklung, aber auch im Backend, eröffnet sie neue Perspektiven und Methoden, um komplexe Datenflüsse zu handhaben. Wenn du deine Programmierfähigkeiten erweitern und eine effizientere Arbeitsweise kennenlernen möchtest, ist es definitiv lohnenswert, die reaktive Programmierung auszuprobieren.
Softwareentwicklung