State Management im Frontend mit Observable Store

Gastbeitrag von | 13.04.2019

Haben Sie auch den Eindruck, dass die Verwendung einiger Frontend JavaScript State Management Pattern irgendwie außer Kontrolle geraten ist? Wer einen Großteil seiner Zeit damit verbringt, Code zu schreiben (und oft auch viel davon), um States innerhalb einer Anwendung zu handhaben, oder sich auf Scaffold Tools verlässt, die 100 oder sogar 1.000 Zeilen Code erzeugen, sollte vielleicht einen Schritt zurückzutreten und sich zu fragen: Brauche ich das wirklich alles? Und natürlich führt dies gleich zur nachfolgenden Frage: Was kann ich tun, um meinen Code zu vereinfachen?

Natürlich könnten wir über meine Ansichten in Bezug auf die Einfachheit einer Software, die Wahl des richtigen Tools für den richtigen Job, die Bedeutung und Berücksichtigung von Wartungskosten, oder die Herausforderungen bei der Nutzung von komplexeren Mustern bei der Arbeit mit Auftragnehmern oder neu eingestellten Kollegen diskutieren, aber lassen Sie mich direkt auf den Punkt kommen:

Das State Management im Frontend muss einfacher werden!

Von vielen Leuten haben ich gehört, dass sie genauso frustriert über einige der Möglichkeiten beim State Management waren, wie ich. Das brachte mich dazu, mit einer einfachen Lösung zu experimentieren, die schließlich zu einem Projekt wurde, das ich Observable Store nenne. Es stellte sich heraus, dass mehrere Leute eine ähnliche Idee hatten. Entsprechende Projekte finden sich auf Github und npm.

Der Observable Store – die Idee entsteht

Einer der Vorteile meines Jobs ist es, dass ich mit vielen Entwicklern in Unternehmen auf der ganzen Welt zusammenarbeiten kann. Dies geschieht in Form von Architekturarbeiten, Trainings und Mentorings, Gesprächen mit Menschen auf Konferenzen, Meetups oder Webinaren. Ich hatte viele Gespräche über verschiedene Möglichkeiten des State Managements und konnte mir anhören, was funktioniert und nicht. Oft hörte ich denn Wunsch nach einer einfacheren Möglichkeit, das State Management in Frontend-Applikation zu handhaben.

Da ich immer wieder mit anderen Architekten und Entwicklern gesprochen und Menschen in ihren Projekten geholfen habe, konnte ich regelmäßig fragen: “Was ist es, was Sie wirklich von einem State Management wollen?”. Hier sind die Hauptziele, die sich aus der Beantwortung dieser Frage ergeben haben:

  1. Eine einzige Quelle der Wahrheit – die sogenannte Single Source of Truth.
  2. Der Status ist schreibgeschützt/unveränderlich.
  3. Bereitstellung von Benachrichtigungen über Zustandsänderungen für jeden Teilnehmer.
  4. Das Nachvollziehen von Zustandsänderungen über den Zeitverlauf.
  5. Das Arbeiten mit kleinen Code-Mengen.
  6. Funktioniert mit jeder Frontend-Bibliothek/Framework (Angular, React, Vue.js oder allem anderen, das JavaScript unterstützt).

Ich begann vor etwa 1,5 Jahren damit, diese allgemeinen Ziele/Konzepte in eine einfache Bibliothek aufzunehmen und entwickelte schließlich etwas, das ich heute Observable Store nenne. Ich verwende es für alle Frontend-Projekte, an denen ich arbeite (React, Vue.js, Angular oder andere), die eine State Management Lösung benötigen. Observable Store erfüllt die oben genannten Ziele, tut dies aber auf äußerst einfache Weise. Der Code für die Bibliothek umfasst insgesamt nur etwa 220 Codezeilen, da die “Leistung”, die sie liefert, aus der Verwendung von RxJS Subjects und Observables stammt. Tatsächlich hat Observable Store nur 1 Abhängigkeit – RxJS.

Warum also Observable Store in Betracht ziehen? Wenn Sie daran interessiert sind, eines der zuvor genannten Ziele zu erreichen, bietet Observable Store einen extrem einfachen Weg, diese Ziele zu erreichen. Sie erhalten sofort einen einzelnen Speicher, der in Ihrer Applikation referenziert werden kann, einen unveränderlichen Zustand (gut für die Erkennung von Änderungen in Bibliotheken/Frameworks), eine Zustandsverlaufsverfolgung und eine Möglichkeit, Speicheränderungen zu abonnieren. Außerdem kann der Observable Store mit jeder JavaScript-Bibliothek oder jedem Framework verwendet werden. Außer an JavaScript sind Sie an nichts gebunden.

Wie fängt man also mit Observable Store an? Hier ein kurzer Überblick.

Erste Schritte mit Observable Store

Um mit dem beobachtbaren Speicher zu beginnen, installieren Sie ihn einfach npm in Ihrem Projekt (Angular, React, Vue.js oder einem beliebigen JavaScript-Projekt):

npm install @codewithdan/observable-store

Von dort aus legen Sie eine Service-Klasse an, die ObservableStore erweitert. Wenn Sie mit TypeScript arbeiten, können Sie generisch die Form der Daten übergeben, die im Store gespeichert werden (übergeben Sie eine Klasse oder ein Interface). TypeScript ist jedoch nicht erforderlich und es funktioniert auch mit ES2015 (oder sogar ES5).

// Optionally define what gets stored in the observable store
export interface StoreState {
    customers: Customer[];
    selectedCustomer: Customer;
    orders: Order[];
    selectedOrder: Order;
}

// Extend ObservableStore and optionally pass the store state
// using TypeScript generics (TypeScript isn't required though)
export class CustomersService extends ObservableStore<StoreState> {
  constructor() {
    // Pass initial store state (if desired). Want to track all
    // changes to the store? Set trackStateHistory to true.
    super(initialStoreState, { trackStateHistory: true });
  }
}

Fügen Sie nun Ihrer Klasse beliebige Funktionen hinzu, um Daten aus einem Datenspeicher abzurufen und mit den Daten zu arbeiten. Rufen Sie setState() auf, um den Zustand im Storage zu setzen, oder getState(), um den Zustand aus dem Storage abzurufen. Beim Setzen des Status können Sie einen Aktionsnamen übergeben, der nützlich ist, wenn Sie Zustandsänderungen und den Zustandsverlauf verfolgen.

import { Observable, of } from 'rxjs';
import { ObservableStore } from '@codewithdan/observable-store';

export class CustomersService extends ObservableStore<StoreState> {
    constructor() { 
        const initialState = {
            customers: [],
            selectedCustomer: null,
            orders: Order[],
            selectedOrder: null
        }
        super(initialState, { trackStateHistory: true });
    }
 
    get() {
        // Get state from store
        const customers = this.getState().customers;
        if (customers) {
            // Return RxJS Observable
            return of(customers);
        }
        else {
            // call server and get data
            // assume async call here that returns Observable
            return asyncData;
        }
    }
 
    add(customer: Customer) {
        // Get state from store
        let state = this.getState();
        state.customers.push(customer);
        // Set state in store
        this.setState({ customers: state.customers }, 
                      'add_customer');
    }
 
    remove() {
        // Get state from store
        let state = this.getState();
        state.customers.splice(state.customers.length - 1, 1);
        // Set state in store
        this.setState({ customers: state.customers } 
                      'remove_customer');
    }
 
}

Wenn sich der Speicherzustand ändert, kann jeder Teil der Anwendung benachrichtigt werden, indem man sich für das stateChanged-Ereignis des Storages anmeldet. In diesem Beispiel werden Änderungen, die der KundenService am Shop vorgenommen hat, empfangen, was eine gute Möglichkeit bietet, einen “Slice” des gesamten Shops mit einem Listener abzuhören.

// Subscribe to the changes made to the store by 
// CustomersService. Note that you'll want to unsubscribe
// when done.
this.customersService.stateChanged.subscribe(state => {
  this.customers = state.customers;
});
Beachten Sie bitte, dass ein stateChanged-Teilnehmer immer ein “frisches” Objekt zurückerhält, da der Speicherzustand unveränderlich ist. Dies eignet sich gut, um Zustands-/Datenänderungen zwischen Bibliotheken/Frameworks zu erkennen. Da RxJS Obeservables hinter den Kulissen verwendet werden, können Sie alle großen Operatoren verwenden, die RxJS auch anbietet.

Wenn Sie alle Änderungen am Shop abfragen müssen, können Sie das globalStateChanged-Ereignis verwenden (danke an Mickey Puri für diesen Beitrag):

// Subscribe to all store changes, not just the ones triggered
// by CustomersService
this.customersService.globalStateChanged.subscribe(state => {
  // access anything currently in the store here
});

Sie können sogar einen bestimmten Teil des Stores (z.B. Kunden und Bestellungen) abhören, indem Sie eine StateSliceSelector-Funktion bereitstellen.

Um Bestellungen zu bearbeiten, können Sie eine weitere Klasse erstellen, die ObservableStore erweitert und die auftragsbezogene Funktionalität hinzufügen. Durch die Aufteilung der Funktionalität in separate Klassen können Sie eine einzelne Verantwortung (das “S” aus den bekannten SOLID-Prinzipien) erreichen, während Sie immer noch nur einen Storage haben, der die gesamte Anwendung unterstützt.

// Extend ObservableStore
export class OrdersService extends ObservableStore<StoreState> {
  constructor() {
    // Define that we want to track changes that this object
    // makes to the store
    super({ trackStateHistory: true });
  }
}
Sowohl CustomersService als auch OrdersService teilen sich den gleichen Storage (wie alle Klassen, die ObservableStore in Ihrer Anwendung erweitern).

Die Observable Store API und die Einstellungen sind einfach zu erlernen und können im Handumdrehen in Betrieb genommen werden. Beispiele für die Verwendung mit Angular- und React-Anwendungen (ich hoffe, in naher Zukunft ein Beispiel für Vue.js hinzufügen zu können) finden Sie im Github-Repo.

Ist Observable Store die Antwort darauf, um das State Management in Frontend-Anwendungen einfach zu halten? Es ist eine potenzielle Lösung, die für mein Unternehmen und mehrere andere Unternehmen/Entwickler, die sie nutzen, gut funktioniert hat. Ich benutze es jetzt seit über einem Jahr privat und genieße wirklich die Einfachheit, die es bietet. Wenn Sie es ausprobieren wollen oder Sie Fragen dazu haben, können Sie gerne einen Kommentar im Github-Repo hinterlassen.

Meine Sicht der Dinge

Wie bereits zu Beginn des Artikels beschrieben, möchte ich mich lieber auf mögliche Lösungen als auf Probleme konzentrieren. Mir ist bewusst, dass manche meine Ansichten teilen und manche sie schlicht ablehnen. Da ich immer wieder nach meiner Meinung gefragt werden, hier eine kurze Zusammenfassung meiner Sicht:

Ich denke, dass wir – und damit meine ich auch mich – oft in den “Gruppendenken”-Modus der Softwareentwicklung verfallen. Das hat große und kleine Folgen, die sich wie ein Lauffeuer schnell in der Entwicklergemeinschaft verbreiten. Ist ein Konzept oder Muster “populär” oder “jeder benutzt es”, ziehen wir es in Betracht, allerdings häufig ohne uns zu fragen, ob es der beste Weg für unser spezifisches Anwendungsszenario ist, ob es tatsächlich notwendig ist, und welche Vor- und Nachteile es dem Team oder Projekt bringt. In manchen Fällen fühlt es sich wie eine “Schaf von der Klippe” Mentalität an.

Da ich im Laufe der Jahre mit verschiedenen Unternehmen auf der ganzen Welt zusammengearbeitet, mit Entwicklern auf Konferenzen gesprochen und mit Menschen online interagiert habe, kann einer der wichtigsten Kniffe, die ich immer wieder höre, wie folgt zusammengefasst werden: “Die Komplexität im Frontend State Management bringt uns um!” Ich höre auch: “Ich kann nicht glauben, wie viel Code zu unserer Anwendung hinzugefügt wird, um dem Muster X zu folgen.”. Oder: “Wir verwenden die Technologie X und Y bei der Arbeit in Teams und können unseren State Management Code nicht zwischen ihnen teilen.”.

In aller Fairness, einige der verfügbaren Muster wie Redux bieten großen Nutzen, bspw. für die Konsistenz in einem Team, den Einblick in den Datenfluss oder ein besseres Debugging. Ich glaube, dass die Vorteile klar und allgemein akzeptiert sind, und viele Entwicklungen die Muster nutzen. Also, wo liegt das Problem?

Zum einen passiert es häufig, dass Entwickler Muster nutzen, ohne wirklich zu verstehen, was die Intention eines Musters ist. Sie kopieren und erweitern es mit eigenem Code. Im Laufe der Zeit steigt die Komplexität der Anwendung und sie verlieren den Überblick. Das passiert oft in Projekten, in denen externe Auftragnehmer, Newbies oder Entwickler, die nicht in der Frontend-Welt zuhause sind, mitarbeiten. Ich habe aber auch die Beobachtung gemacht, dass es bei Frontend-Entwicklern nicht immer besser aussieht.

Man kann argumentieren, dass jeder, der ein Muster verwendet, Zeit braucht, um das Muster genau zu verstehen. Das ist ein valider Punkt. Doch es gibt in der Praxis oft Situationen, in denen ein Muster bereits in einem Projekt verwendet wird, der Zeitdruck steigt, und es somit keine Chance gibt, eine Alternative zu durchdenken. Also wird das Muster weiter verwendet, auch wenn der Entwickler nicht ganz versteht, was vor sich geht. Vielleicht sollten sich daher Entwicklungsabteilungen grundsätzlich fragen, ob es sich überhaupt lohnt, einen solchen Weg zu beschreiten. Es geht hier lediglich um das State Management, die Entwicklungsmannschaft darf sich aber auch noch um den Rest der Anwendung kümmern.

Neben dem mangelnden Verständnis gibt es einen weiteren Aspekt zu bedenken: Ist es möglich, den gleichen Code mit verschiedenen JavaScript-Frontend-Technologien zu verwenden und sieht der Code gleich aus? Zum Beispiel hat React Redux, Angular hat NgRx (Redux + RxJS), Vue.js hat Vuex, und so weiter. Das mag vielleicht für Sie kein Problem sein, aber für mehrere Unternehmen, mit denen ich zusammenarbeite, ist es ein Problem, denn diese wollen keine unterschiedlichen Implementierungen desselben Gesamtmusters pflegen.

Die Frage nach dem gleichen Code zwischen verschiedenen JavaScript-Frontend-Technologien kann ich mit einem eindeutigen “Nein” beantworten. Die gemeinsame Nutzung von Code ist in den meisten Szenarien, die ich gesehen habe, oft keine Option. Das verwendete Muster kann in einigen Fällen ähnlich sein, aber die Implementierungen unterscheiden sich radikal zwischen den Bibliotheken/Frameworks. Wenn Ihr Unternehmen nicht nur eine Hauptbibliothek/Framework für Frontend-Projekte verwendet, kann das eine Herausforderung darstellen, insbesondere wenn Sie versuchen, Projekte so konsistent wie möglich zu gestalten (und gleichzeitig den Entwicklern die Möglichkeit geben, die von ihnen bevorzugte Technologie zu nutzen).

Darüber hinaus gibt es sicherlich zusätzliche Herausforderungen, auf die ich mit komplexeren State Management Optionen hinweisen könnte, wie bspw. Wartungsherausforderungen, die schiere Menge an hinzugefügtem Code, Paketgrößen, Teamwissen usw. Für mich läuft es darauf hinaus, das richtige Werkzeug für den richtigen Job zu verwenden und zu erkennen, dass nicht alles ein Nagel ist, der einen komplexen Hammer erfordert.

Ich glaube, dass es sich lohnt darüber nachzudenken, ob ein Muster für ein bestimmtes Szenario tatsächlich geeignet oder zu komplex ist und es ggf. tragfähigere Alternativen gibt. Aus meiner Erfahrung weiß ich: Ein Muster passt NIE für alle Situationen und es gibt viele Anwendungen da draußen, die komplexe Muster verwenden, die an sich nicht benötigt werden. Ich habe es selbst oft in Unternehmen gesehen; beispielsweise kann eine Anwendung Standard-CRUD-(Create, Read, Update, Delete)-Operationen direkt an einen Backend-Service durchführen. Sobald eine Operation abgeschlossen ist, ist sie abgeschlossen. Abgesehen davon, dass dem Benutzer eine Nachricht angezeigt wird, gibt es aus Zustandssicht nichts anderes zu tun. In diesem und vielen anderen einfachen Szenarien ist eine komplexe State Management Lösung oft nicht erforderlich – sie würde nur unnötige Komplexität schaffen. Was mich zu meinen drei Lieblingswörtern bringt: Keep it simple.

Ich bewundere Architekten und Entwickler, die über die Weisheit, das Wissen, die Expertise und die Fähigkeit verfügen, ihren Anwendungscode so einfach wie möglich zu halten und gleichzeitig die Bedürfnisse der Benutzer zu erfüllen. Gute Software zu entwickeln ist hart, und die Fähigkeit, Code einfach zu halten, ist wohl genauso schwer. Es ist eine Kunst und Fähigkeit, die im Laufe der Zeit entwickelt werden muss, und in einigen Fällen habe ich das Gefühl, dass diese Fähigkeit verloren gegangen ist. Die Einfachheit der Dinge bringt am Ende viele positive Ergebnisse – vor allem bei der langfristigen Instandhaltung.

 

Hinweise:

Interessieren Sie sich für weitere Tipps aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.

Jede Situation ist anders und Dan Wahlin ist immer daran interessiert, unterschiedliche Meinungen und konstruktive Gedanken zu hören. Der Beitrag ist im Original unter https://blog.codewithdan.com/simplifying-front-end-state-management-with-observable-store/ erschienen. Dort können Sie gerne einen Kommentar hinterlassen.

Mit Zustimmung von Dan Wahlin übersetzen wir verschiedene Beiträge von ihm hier in unserem Blog vom Englischen ins Deutsche. Beiträge im Original finden Sie unter https://blog.codewithdan.com/. In LinkedIn können Sie sich mit ihm unter https://www.linkedin.com/in/danwahlin/ vernetzen.

Dan Wahlin
Dan Wahlin

Dan Wahlin ist Entwickler, Architekt, Technologietrainer, Autor und öffentlicher Redner mit Fachkenntnissen in der Architektur, dem Design und dem Bau von lokal und in der Cloud gehosteten Webanwendungen unter Verwendung verschiedener Technologien (JavaScript, TypeScript, Angular, .NET, C #, ASP.NET) , Node.js, HTML5 / CSS, Docker, Azure, Github / git, Windows / OSX / Linux und mehr). Er ist Autor mehrerer Bücher, verfasste Hunderte von technischen Artikeln und erstellt Videokurse für Pluralsight und Udemy. Dan spricht gerne bei Anwendergruppen, Meetups, Code-Camps und Konferenzen auf der ganzen Welt, darunter Microsoft BUILD und (früher) TechEd, MIX, DevIntersection, AngleBrackets, DevSum, Ng-Conf, Angular U und viele andere.