Zum Inhalt springen

+State – wo die Fachlichkeit lebt

Der Infrastructure-Artikel hat aus einem rohen "ACTIVE" ein eigenes CustomerStatus.Active gemacht.

Mehr aber auch nicht.

Wir wissen damit noch nicht, ob dieser Kunde bestellen darf. Wir wissen nicht, welche Aktionen gerade zur Verfügung stehen, welcher Zustandsübergang erlaubt ist oder welche Auswahl für einen fachlichen Vorgang relevant ist.

Hier beginnt +State.

Infrastructure übersetzt die Außenwelt in eine kontrollierte eigene Repräsentation. +State setzt diese Werte in Beziehung und interpretiert sie im Kontext unserer fachlichen Welt.

Ein +State sieht auf den ersten Blick oft harmlos aus:

type CustomerState = {
customers: Customer[];
selectedCustomerId?: CustomerId;
loading: boolean;
};

Für sich genommen enthält dieser State zunächst Werte. Interessant wird er durch die Fragen, die sich mit diesen Werten beantworten lassen:

Welcher Kunde ist ausgewählt?
Ist die Auswahl noch gültig?
Welche Kunden erfüllen eine bestimmte Bedingung?
Welche Aktionen sind aktuell erlaubt?
Was folgt aus einem Zustandswechsel?

Nicht jede dieser Fragen ist automatisch eine Business Rule. Auch nicht jedes Feld eines Stores ist zwangsläufig fachlicher Zustand. Ein loading-Flag bleibt beispielsweise technischer Laufzeitzustand – auch wenn es, wie der Denkzettel Loading ist kein Boolean zeigt, selten ein einfacher Schalter ist.

Der entscheidende Unterschied liegt darin, dass +State nicht beim Speichern von Werten aufhört. Hier entstehen Zustandsübergänge, Ableitungen und fachliche Entscheidungen aus dem aktuellen Zustand der Anwendung.

State bekommt durch seine Beziehungen, Regeln und Ableitungen Bedeutung.

+State verbindet Zustand mit Business Rules, Ableitungen und Zustandsübergängen und erzeugt daraus fachlich bedeutende Ergebnisse.

Eine sehr einfache Vorstellung von State Management sieht so aus:

API
Store speichert Response
Component liest Response

Das kann völlig ausreichend sein. Architektonisch ist der Store in diesem Modell allerdings kaum mehr als ein Cache für Backend-Daten. Er hält Werte bereit, ohne ihnen eine eigene Bedeutung zu geben.

Unser Verständnis von +State geht weiter:

Infrastructure
+State
├── besitzt Zustand
├── verarbeitet fachliche Intents
├── wendet Business Rules an
├── modelliert Zustandsübergänge
└── erzeugt Ableitungen

Ob dafür Signals, NgRx, Redux, Zustand, RxJS oder ein anderes State-Konzept verwendet wird, ändert an dieser Verantwortung nichts.

Die Library entscheidet, wie State verwaltet wird. Die Architektur entscheidet, welche Verantwortung dieser State trägt.

Infrastructure hat übersetzt:

"ACTIVE"
CustomerStatus.Active

+State kann diesen Zustand jetzt gemeinsam mit weiteren Informationen auswerten:

CustomerStatus.Active
+
CreditHold.None
+
Account.enabled
canOrder

Die Frage, ob ein Kunde bestellen darf, ist keine technische Übersetzung mehr. Sie beschreibt eine Regel unseres Systems.

Dasselbe gilt für Fragen wie:

  • Darf ein Auftrag storniert werden?
  • Darf ein Datensatz gelöscht werden?
  • Ist ein Zustandsübergang zulässig?
  • Welche Aktion steht in der aktuellen fachlichen Situation zur Verfügung?
  • Erfüllt eine Entity eine bestimmte fachliche Bedingung?

Eine Business Rule wird nicht zu Presentation-Logik, nur weil ihr Ergebnis später einen Button deaktiviert.

Gerade dort schleichen sich fachliche Regeln trotzdem häufig ein:

disabled = customer.status !== CustomerStatus.Active || customer.creditHold !== CreditHold.None;

Solange nur eine Oberfläche existiert, wirkt das harmlos.

Spätestens wenn dieselbe Entscheidung zusätzlich in einer Ionic-Presentation, einer mobilen Oberfläche oder an einer anderen Stelle benötigt wird, existieren mehrere Implementierungen derselben Regel.

Der fachliche Zustand kann stattdessen eine zentrale Aussage bereitstellen:

canOrder;

Die Presentation entscheidet anschließend, wie sie diesen Zustand darstellt: als deaktivierten Button, ausgegraute Aktion oder gar nicht sichtbaren Menüeintrag.

Ob die Aktion fachlich erlaubt ist, entscheidet sie nicht erneut.

Nicht jede Information muss zusätzlich gespeichert werden.

Aus:

customers
+
selectedCustomerId
selectedCustomer

kann selectedCustomer abgeleitet werden.

Dasselbe gilt beispielsweise für:

customers
+
filter
+
sort
visibleCustomers

selectedCustomer und visibleCustomers müssen nicht als weitere Kopien ihres Ursprungszustands gehalten und bei jeder Änderung manuell synchronisiert werden.

Sie können aus dem vorhandenen State entstehen.

Das reduziert doppelte Wahrheit und die damit verbundenen Synchronisationsprobleme. Sobald zwei gespeicherte Werte dieselbe Information ausdrücken, muss irgendein Mechanismus dafür sorgen, dass beide dauerhaft zusammenpassen. Eine Ableitung benötigt diese Synchronisation nicht.

Aus gespeichertem Source State wie Kunden, Auswahl, Filter und Sortierung werden Werte wie selectedCustomer, visibleCustomers und canOrder abgeleitet.

Wie sich Query State und Derived State in der Praxis voneinander unterscheiden lassen und warum Filter und Sortierung dabei selbst zum State gehören können, beschreibt Reaktive Filter und Sortierung ausführlicher.

Auch eine Auswahl muss nicht bedeuten, dass eine komplette Entity ein zweites Mal gespeichert wird.

entities
+
selectedId
selectedEntity

Wenn die Auswahl für den fachlichen oder anwendungsweiten Zustand relevant ist, kann die ID als Source State genügen. Die ausgewählte Entity entsteht anschließend als Ableitung.

Ob eine Auswahl überhaupt in +State gehört, hängt dabei von ihrer Bedeutung ab. Ein kurzzeitig hervorgehobener Tabellen-Eintrag kann reine Presentation sein. Eine Auswahl, auf der weitere fachliche Operationen aufbauen, hat eine andere Lebensdauer und Verantwortung.

Wie ein solcher Select-Flow konkret geschnitten werden kann, zeigt Select by ID.

Mehrere Quellen können zu einer Projektion werden

Abschnitt betitelt „Mehrere Quellen können zu einer Projektion werden“

Ableitungen müssen nicht auf eine einzelne Source beschränkt bleiben.

articles
+
authors
+
categories
Projektion

Mehrere State-Sources können gemeinsam eine konsumierbare Projektion ergeben.

Entscheidend ist dabei weniger, wie viele Stores oder Quellen beteiligt sind, sondern wo die Verantwortung für die entstehende Information liegt.

Wie mehrere reaktive Quellen zusammenspielen können, zeigt ViewModel aus mehreren Datenquellen. Projektionen, die über Ereignisse aufgebaut und aktualisiert werden, behandelt Event-driven Projection.

Beide Varianten zeigen denselben grundsätzlichen Gedanken: Consumer müssen nicht zwangsläufig die Rohstruktur des zugrunde liegenden States kennen.

Aus der bisherigen Argumentation sollte nicht folgen, dass ein Feature genau einen großen +State besitzen muss.

Store-Grenzen entstehen nicht primär durch Dateigröße oder dadurch, dass mehrere Zustände zufällig denselben Entity-Typ verwenden.

Relevanter sind Fragen wie:

  • Welche Verantwortung gehört zusammen?
  • Haben die Zustände denselben Lebenszyklus?
  • Ändern sie sich aus denselben Gründen?
  • Wer besitzt sie?
  • Welche Consumer verwenden sie?

Zwei Zustände können denselben Customer verwenden und trotzdem unterschiedliche Verantwortungen haben.

Umgekehrt können mehrere Datentypen gemeinsam zu genau einem fachlichen Zustand gehören.

Wenn der Read Store zu groß wird behandelt diese Grenzen ausführlich.

Für +State ist vor allem wichtig: Der Layer beschreibt eine Art von Verantwortung, nicht die Forderung nach einem einzigen Store.

+State besitzt nicht nur lesbaren Zustand. Hier treffen auch fachliche Änderungsabsichten auf den aktuellen Zustand.

deleteRequested
aktueller Zustand
zulässig / nicht zulässig
Zustandsänderung

Oder:

completeOrder
Business Rule
State Transition

Ein Intent beschreibt zunächst eine Absicht.

„Dieser Kunde soll gelöscht werden“ ist etwas anderes als „Dieser Kunde wurde gelöscht“.

Ob die gewünschte Änderung zulässig ist und welcher fachliche Zustand daraus entsteht, hängt von den Regeln und dem aktuellen Zustand ab.

Wie sich Create-, Retrieve-, Update- und Delete-Flows entlang solcher Verantwortungen schneiden lassen, beschreibt die CRUD-Slices-Serie ausführlich.

Die wichtige Grenze für diesen Artikel ist einfacher: Eine fachliche Änderungsabsicht sollte nicht erst in einer Component oder einem HTTP-Service ihre Bedeutung bekommen.

+State kann Zustand und Ableitungen liefern, aus denen später ein ViewModel entsteht.

Dabei muss nicht jede View-Projektion bereits vollständig im Store gebaut werden.

Eine Projektion kann fachlichen Zustand konsumierbar machen. Die Application-Schicht kann anschließend mehrere solcher Informationen für eine konkrete View zusammenführen oder ihr eine passend zugeschnittene API bereitstellen.

Wo diese Grenze sinnvoll liegt, hängt vom jeweiligen Schnitt ab.

ViewModel Aggregation behandelt ausführlicher, wie ViewModels aus mehreren Datenquellen entstehen können, ohne Backend-Strukturen oder interne State-Details bis in die Presentation durchzureichen.

Für +State bleibt entscheidend, dass fachliche Regeln und fachlich relevante Ableitungen nicht erst beim Bau einer konkreten Oberfläche entstehen.

+State darf und soll beispielsweise:

  • fachlich relevanten Laufzeitzustand besitzen
  • Business Rules anwenden
  • fachliche Zustandsübergänge modellieren
  • fachliche Intents verarbeiten
  • Derived State erzeugen
  • relevante Selektionen bereitstellen
  • fachliche Filterungen und Projektionen erzeugen
  • auf fachlich relevante Ereignisse reagieren
  • Regeln zentralisieren, die unabhängig von der konkreten Presentation gelten

+State sollte nicht:

  • HTTP-Requests selbst implementieren
  • DTOs parsen
  • fremde API-Contracts durch die Fachlichkeit reichen
  • HttpClient, fetch oder SDK-spezifische APIs bedienen
  • technische API-Fehlercodes interpretieren
  • den Router direkt steuern
  • Dialoge öffnen
  • Snackbars oder Toasts anzeigen
  • DOM- oder UI-Framework-Details kennen
  • zwischen Angular Material, Ionic oder Bootstrap unterscheiden
  • konkrete View-Orchestrierung übernehmen, die zur Application gehört

Bei Grenzfällen hilft eine einfache Frage:

Würde diese Regel auch dann noch gelten, wenn Material morgen durch Ionic ersetzt wird?

Wenn ja, ist sie ein guter Kandidat für +State.

Das ist keine Definition, aber eine erstaunlich brauchbare Leitlinie.

Die Vorteile dieses Schnitts werden besonders sichtbar, wenn mehrere Oberflächen dieselbe Fachlichkeit verwenden.

In einem Langzeitprojekt wurden Angular Material und Ionic über Jahre parallel auf derselben fachlichen Basis betrieben.

Das funktionierte, weil Business Rules nicht in beiden Presentations separat implementiert wurden.

+State
/ \
/ \
Material Ionic

Die Presentation entscheidet über Darstellung. +State stellt die fachlichen Aussagen bereit.

Wäre canOrder in beiden Presentations separat berechnet worden, hätten zwei Implementierungen dauerhaft synchron gehalten werden müssen. Früher oder später hätten beide Oberflächen unterschiedliche Antworten auf dieselbe fachliche Frage liefern können.

Eine gemeinsame +State-Schicht stellt dieselben fachlichen Regeln und Ableitungen für eine Angular-Material- und eine Ionic-Presentation bereit.

Aus derselben Trennung entstehen weitere Vorteile.

Business Rules werden auffindbar. Eine fachliche Entscheidung, die über Components, Pipes, Form Validators, Services und Templates verteilt ist, lässt sich nicht mehr an einer Stelle nachvollziehen. Man muss sie rekonstruieren.

Wer weiß, dass canOrder im fachlichen State entsteht, weiß auch, wo er nach der Regel suchen muss.

Ableitungen bleiben konsistent, weil nicht mehrere Kopien derselben Information manuell synchronisiert werden müssen.

Und Business Rules lassen sich unabhängig von einer konkreten Oberfläche testen. Ob Material einen Button rendert oder Ionic einen Menüpunkt, ist für die Entscheidung canOrder irrelevant.

Diese Vorteile entstehen nicht durch die verwendete State-Library. Sie entstehen durch die Grenze.

Eine klare Heimat für Fachlichkeit bedeutet nicht, jede Variable in einen Store zu verschieben.

Dinge wie:

isDialogOpen
hoveredRow
activeTab
tooltipVisible
accordionExpanded

sind häufig Presentation State. Sie beschreiben, wie eine konkrete Oberfläche gerade dargestellt wird.

Auch diese Grenze ist nicht in jedem Fall absolut. Ein activeTab kann beispielsweise irgendwann eine fachlich relevante Auswahl repräsentieren. Der Name der Variable entscheidet darüber wenig.

Hilfreicher ist eine andere Frage:

Hat dieser Zustand Bedeutung unabhängig von seiner aktuellen Darstellung?

Wenn die Antwort nein lautet, ist +State wahrscheinlich nicht sein natürlicher Ort.

+State besitzt fachlich relevanten Zustand, Business Rules, Ableitungen, Selektionen, Zustandsübergänge und fachliche Intents.

Eine konkrete View sollte deshalb trotzdem nicht den gesamten State kennen müssen.

Eine Customer-List-View benötigt vielleicht nur:

customers
loading
selectCustomer()
createCustomer()

Eine Detail-View benötigt einen anderen Ausschnitt derselben Fachlichkeit.

Damit entsteht die nächste Verantwortungsgrenze: +State besitzt und interpretiert die Fachlichkeit. Die Application schneidet daraus eine API zu, die zu einem konkreten Anwendungsfall oder einer konkreten View passt.

Die Fachlichkeit bleibt dadurch an einem Ort, ohne dass jede Presentation ihre gesamte innere Struktur kennen muss.