Grundlagen des State Design Patterns
Mit dem State Design Pattern kann der Zustand von Objekten modelliert werden. Dazu werden
aus einer als Context bezeichneten Klasse die zustandsabhängigen Teile in einzelne
sogenannte State-Klassen ausgelagert.
Wie in der untenstehenden Grafik zu sehen ist, implementieren alle State-Klassen ein
gemeinsames State Interface.
Abbildung 1: State-Klassen und State Interface
Der Context hat eine Referenz auf dieses Interface. Das ist der aktuelle Zustand des
Contexts. Er implementiert ebenfalls das State-Interface und leitet alle Aufrufe an
seinen aktuellen State weiter.
Um Zustandswechsel zu ermöglichen, erhält der Context einen Setter für den aktuellen
State und wird den konkreten State Implementierungen per Constructor injiziert. Jeder
State bestimmt also seine Folgezustände, indem er den Setter auf dem Context
aufruft.
Durch das Design Pattern werden Entscheidungsstrukturen wie if-else oder switch-case
in
der Context-Klasse durch polymorphe Strukturen ersetzt. Das macht die einzelnen Klassen
kleiner, einfacher zu verstehen und zu testen. Ferner kann das System leicht um weitere
Zustände erweitert werden, ohne dass dabei der Context selbst angepasst werden muss.
Anwendungsbeispiel in Android
Seit Android 6.0 werden bestimmte Berechtigungen (im Folgenden Permissions genannt) vom
Benutzer erst während der Nutzung angefordert. Die App sollte also unabhängig davon, ob
die Permission erteilt wurde oder nicht, funktionsfähig sein.
Eine dieser speziellen, als gefährlich eingestuften Permissions ist der Zugriff auf den
aktuellen Standort des Gerätes per GPS. Wenn man nun also eine Suche implementieren
will, welche einem z. B. alle Parkplätze in der Nähe zeigt, dann muss man den Benutzer
nach der Permission fragen, um auf seinen Standort zugreifen zu können.
Abhängig davon, ob der Benutzer die Permission erteilt oder nicht, muss also die Suche
ausgeführt werden. Der einfachste Ansatz hierfür ist in der LocationSearch Klasse
ein
boolean Flag zu definieren, welches abhängig von der Nutzerauswahl gesetzt wird.
Beachtet werden muss jedoch, dass der Nutzer zusätzlich die Möglichkeit hat, das GPS in
den Einstellungen seines Android Gerätes zu deaktivieren. Um dies abzubilden, würde die
LocationSearch Klasse also ein weiteres Flag benötigen.
Diese Implementierung führt dazu, dass an vielen Stellen in der LocationSearch
Klasse
abhängig von den beiden Flags anderer Code ausgeführt wird. Das sorgt dafür, dass die
LocationSearch Klasse relativ groß und unübersichtlich wird. Die Wartbarkeit ist
gering,
der Code ist schwer an neue Anforderungen anzupassen und eine hohe Testabdeckung für
diese Klasse zu erreichen, ist relativ kompliziert und aufwendig.
Doch wie kann nun das State Design Pattern eingesetzt werden, um dies zu vermeiden?
Zuerst müssen alle denkbaren Zustände erfasst werden. Dazu werden GPS- und
Permission-Zustand getrennt voneinander betrachtet, wie in der nachfolgenden Grafik
dargestellt.
Abbildung 2: GPS- und Permission-Zustand getrennt
Um das Pattern anwenden zu können, müssen nun die beiden getrennten Zustände kombiniert
werden. Dabei ergeben sich folgende Zustände:
- Permission granted - GPS On
- Permission granted - GPS Off
- Permission not granted - GPS On
- Permission not granted - GPS Off
Nun wird untersucht, welche Zustandswechsel (im Folgenden Transaktionen genannt) möglich
sind. Diese sind nachfolgend grafisch dargestellt.
Abbildung 3: GPS- und Permission-Zustand kombiniert
Das Diagramm zeigt die einzelnen Zustände und die möglichen Transaktionen. In diesem
Beispiel ist es möglich das GPS an- und auszuschalten. Weiterhin besteht die
Möglichkeit, die Permission zur Nutzung des GPS zu erteilen oder zu entziehen. Diese
vier Transaktionen werden als Methoden in unser State Interface aufgenommen.
Transaktionen, die für einen Speziellen State keine Handlung erfordern, werden einfach
leer implementiert. Also der State PermissionGrantedGPSoff implementiert die
Methoden
off und permissionGranted zum Beispiel leer.
Die vom Zustand abhängige Logik kann nun in die entsprechenden State Klassen verschoben
werden. Dadurch entstehen kleinere Klassen, die leichter zu überblicken und zu testen
sind. Das komplette Klassendiagramm zum Beispiel ist in Abbildung 4 zu sehen.
Abbildung 4: Diagramm aller Klassen