State Pattern-CLI: Aufbau des Patterns und Navigationsstruktur

https://bildung.social/@oerinformatik/110504024656041988

https://oer-informatik.de/state-pattern-cli1

tl/dr; (ca. 20 min Lesezeit): Am Beispiel eines Programms mit Kommandozeilenmenü wird das State-Pattern beispielhaft in Java implementiert: Eine Fingerübung, um das Pattern, Transitionen und die Elemente des UML-Zustandsdiagramms zu verinnerlichen. Im ersten Teil geht es zunächst um den Aufbau des Patterns selbst und die Grundstruktur. (Zuletzt geändert am 11.06.2023)

Der Artikel ist Bestandteil einer mehrteiligen Reihe:

  1. Aufbau des State-Patterns allgemein und grundlegende Struktur (dieser Artikel)
  2. Realisierung der Zustände als Singletons (per Java-ENUMS)
  3. Realisierung der Transitionen und deren Trigger
  4. Realisierung von Effekten und Guards von Transitionen

Ausgangspunkt: Das UML-State-Diagramm

Vorneweg: wir sind uns einig: Java ist auch nicht meine erste Wahl für eine Konsolen-App. Sinn und Zweck dieses Tutorials ist nich, für einen bestehenden Anwendungsfall eine nutzbare App zu erstellen. Ziel ist vielmehr, mit einer kleinen Fingerübung einen Einblick zu gewinnen in das State-Pattern. Ausversehen nutzen wir dazu eine ganze Reihe Notationmittel der UML und tauchen ein in die OOP mit Java, Interfaces, Abstrakten Klassen, Lambda-Ausdrücken und Generics.

Wenn in einem User-Interface (egal ob CLI, GUI oder andere) unterschiedliche Menüs, Einträge oder Dialoge geöffnet sind, reagiert das System jeweils anders auf Eingaben. Es befindet sich also in unterschiedlichen Zuständen: bei einer Webapp macht es beispielsweise einen gravierenden Unterschied, ob der Zustand der App “als Nutzer angemeldet” oder “als Gast aktiv” ist. Daher lässt sich die Navigation in einem User-Interface sehr gut mit UML-Zustandsdiagrammen planen. Um das Programm einfach zu halten modellieren wir ein Kommandozeilen-Programmfragment, dass beispielhaft mit Java realisiert werden soll. Die Struktur ist bei Graphischen Benutzeroberflächen (GUI) aber genauso.

Basis soll ein rudimentäres User-Interface sein, das für eine Adresse oder eine andere x-beliebige Entity (Datenhaltungsklasse) einfache CRUD-Masken zum Erstellen (Create), Lesen (Read), Ändern (Update) und Löschen (Delete) bereitstellt. Wenn das Programm gestartet ist und ich angemeldet bin, erhalte ich eine Liste der eingetragenen Werte und von dort in die anderen Masken wechseln, die die Detailverarbeitung übernehmen sollen. Folgendes UML-Zustandsdiagrammen modelliert die geplante Navigation des User-Interfaces:

Zustandsdiagramm der Kommendozeilen-Benutzerführung
Zustandsdiagramm der Kommendozeilen-Benutzerführung

Es soll eine möglichst einfache Navigation umgesetzt werden: per Tastendruck wird ein Menüpunkt ausgewählt. Beispielsweise triggert "a" in jedem Zustand den Abbruch. Es sind zunächst nicht allzu viele innere Aktionen modelliert, um das Beispiel klein zu halten. Wir wollen nur prototypisch die Realisierung dieser Zustandswechsel mit Java unter Zuhilfenahme des State-Patterns zeigen.

Allgemeine Formulierung des State Pattern

Das State-Pattern ist eines der Gang-of-Four-Pattern und es ermöglicht es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert.

Ein Beispiel: das Verhalten der Methode reagiereAufAnstupsen() ändert sich abhängig davon, ob das Objekt im Zustand Wach oder Schlafend ist.

Im State Pattern realisiert wird das, in dem der Zustand des Objekts in eigene Klassen ausgelagert wird, die das Verhalten (also die Implementierung der Methoden) dann an den Zustand anpassen.

UML-Klassendiagramm, in dem das Verhalten der Klasse UnterhaltungsKontext über das Interface State in den Implementierungen von State mit Namen Wach und Schlafend realisiert wird
UML-Klassendiagramm, in dem das Verhalten der Klasse UnterhaltungsKontext über das Interface State in den Implementierungen von State mit Namen Wach und Schlafend realisiert wird

Wie sich ein Objekt der Klasse UnterhaltungsKontext verhält, wenn die Methode reagierenAufAnstupsen() ausgeführt wird, bestimmt das Objekt, welches derzeit mit dem Attribut zustand referenziert wird. Im obigen Beispiel ist zustand lose gekoppelt über das Interface State und kann daher Instanzen von Wach oder Schlafend referenzieren. Sollte neben den individuellen Methoden reagierenAufAnstupsen() noch gemeinsames Verhalten von Wach und Schlafend existieren, könnte man an Stelle des Interfaces auch eine abstrakte Klasse nutzen und hier gemeinsam genutzte Attribute und Methoden implementieren. In der Beschreibung der Gang of Four wird eine abstrakte Klasse genutzt.

Den Instanzwechsel habe ich oben vereinfachend realisiert, in dem wecken() und einschalfen() jeweils neue Objekte von Wach und Schlafend erstellen - hier sollten natürlich bestehende Objekte weitergenutzt werden.

Realisierung für die Kommandozeilen-Benutzeroberfläche

Entwurf des Klassendiagramms für die konkrete Implementierung

Erster Schritt: Welche Zustände benötigen wir, welche Aktivitäten müssen bereitgestellt werden und was ist unser Kontext? Mithilfe des Zustandsdiagramms lassen sich die Zustandsklassen, der Kontext und das Interface entwerfen. Wir vereinfachen die Funktionalität zunächst stark: jeder Zustand hat je eine Aktivität, die bei Eintritt in den Zustand (entry()), während der Zustand aktiv ist (doing()) und bei Austritt aus dem Zustand (exit()) ausgeführt wird. Aus dem Zustandsdiagramm ergeben sich noch einige spezialisierte Aktivitäten einiger Zustandsklassen. Es entsteht folgendes UML-Klassendiagramm:

Der Grundlegende Aufbau des State-Patterns angepasst für das CLI
Der Grundlegende Aufbau des State-Patterns angepasst für das CLI

Dreh- und Angelpunkt: die Kontextklasse UIContext und deren Zustandswechsel

Der Kontext ist das Objekt, dessen innerer Zustand das Verhalten ändert und deswegen ausgelagert wird. In unserem Fall ist das die Kommandozeilen-Benutzeroberfläche (UI).

Wir implementieren ein aggregiertes Zustandsobjekt state mit Setter, einen Konstruktor, die drei Aktivitäten entry(), doing(), exit(), deren Verhalten jeweils an den aktiven Zustand delegiert werden und eine main()-Methode, mit der wir das Programm starten und in der wir ein Objekt unserer Benutzeroberfläche erzeugen:

Die Implementierung des Interfaces ist im obigen Klassendiagramm bereits vorgegeben:

Abstrakte Klasse mit Default-Implementierungen des Interfaces (Klasse AbstractUIState)

Zweiter Schritt: Wir machen uns das Leben ein wenig einfacher. Für den ersten Schritt ist es noch nicht erforderlich, die im Interface deklarieren Methoden (entry(), doing(), exit()) individuell zu implementieren. Es bietet sich daher an, eine abstrakte Klasse mit default-Implementierungen voran zu schalten:

Die vorgeschaltete abstrakte Klasse AbstractUIState
Die vorgeschaltete abstrakte Klasse AbstractUIState

Zunächst werden wir noch keine Aktivitäten für entry(), doing() und exit() definieren, sondern lediglich über Ausgaben prüfen, welche Zustände jeweils genutzt werden.

Wir nutzen dazu das Java-Reflection-Feature: Java bietet Klassen und Methoden an, die Meta-Informationen über die aktuellen Objekte, liefern - this.getClass().getSimpleName() beispielsweise den Namen der genutzten Klasse. Wir können uns also ausgeben lassen, welche Spezialisierung der Klasse gerade aktiv ist, ohne den Code in den Spezialisierungen implementieren zu müssen.

Die spezialisierten Zustandsklassen

Die spezialisierten Zustandsklassen sollen hier nur rudimentär implementiert werden. Einen großen Teil der Logik erben Sie bereits von der AbstractUIState-Klasse, sodass lediglich abweichende Implementierungen oder spezielle Methoden, die die Elternklasse nicht hatte, ausimplementiert werden müssen. Beispielhaft sei dies an den Zuständen AnmeldenUI und ListUI gezeigt, die jeweils nur eine zusätzliche Methode (login() bzw. abmelden()) neu implementieren.

Der erste Test

Damit wäre das State-Pattern grundsätzlich fertig implementiert und wir können ausprobieren, ob der Zustandswechsel auch zu einem Verhaltenswechsel führt. In der main() Methode hatten wir bereits ein Objekt unseres Kontexts erstellt. Wir müssen jetzt nur per Setter-Methode den Zustand ändern und einige Kontext-Methoden aufrufen, um zu überprüfen, dass die Implementierung der jeweiligen Zustandsklassen genutzt wird.

Wie erwartet delegiert das Kontext-Objekt die Abarbeitung der Methoden entry(), doing() und exit() an das jeweilige Zustandsobjekt. Die rudimentäre Ausgabe des Programms verrät uns, welche Klasse sich jeweils um die Methoden gekümmert hat:

<<< Entering state AnmeldenUI
--- Being in state AnmeldenUI
>>> Leaving state AnmeldenUI
<<< Entering state ListUI
--- Being in state ListUI

Nachdem die Grundzüge funktionieren können wir uns im Folgenden um eine Umgruppierung der einzelnen Zustandsklassen kümmern.

Weiter in Teil 2


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: State Pattern-CLI: Aufbau des Patterns und Navigationsstruktur” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/state-pattern-cli1 veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/design-pattern/state-pattern. Stand: 11.06.2023.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: