Das State Design Pattern
Studienprojekt von Philipp Hauer. 2009 - 2010. ©
Strategy, Observer, Decorator, Factory Method, Abstract Factory, Singleton, Command, Composite, Facade, State
Literaturverzeichnis, Philipps Blog
Inhalt
Einführung
Wir wollen unsere Freundin (oder wahlweise den Freund) modellieren. Wer denkt, dies sei praxisfern, der irrt. Man denke nur an das erfolgreiche PC-Spiel "Die Sims". Die folgende Modellierung könnte dort realisiert worden sein.
Nun, wir können mit unserer Freundin interagieren: Wir können uns mit ihr unterhalten, ihr einen Kuss geben oder sie ärgern. Je nach ausgeführter Aktion wird sich ihre Laune (ihr Zustand) ändern: Neutral, Bockig oder Fröhlich. Küsst man eine neutralgelaunte Freudin, wird sie fröhlich. Ärgert man sie, wird sie bockig.
Die Zustände und die Zustandsübergänge unserer Freundin seien in folgendem Zustandsautomat illustriert:
Je nach Zustand wird die Freundin unterschiedlich auf Interaktionen (unterhalten(), verärgern(), kussGeben()) reagieren. Die Realität lehrt uns, dass es sich mit einer bockigen Freundin anders unterhält, als mit einer fröhlichen. Das äußere Verhalten der Freundin ändert sich also in Abhängigkeit von ihrem inneren Zustand.
Versetzen wir uns gedanklich in die 1970er Jahre: zur Blütezeit der Strukturierten Programmierung. Wie hätte man damals das Problem gelöst? Der aktuelle Zustand würde durch eine Integer-Variable repräsentiert werden. 0 steht für Neutral, 1 für Bockig und 2 für Fröhlich. In jeder Operation (unterhalten(), kussGeben(), verärgern()) wird zunächst geprüft, welchen Wert diese Integer-Variable hat und entsprechend wird ein Verhalten ausgeführt.
class Freundin{
//Mögliche Launen der Freudin
private static final int NEUTRAL = 0;
private static final int BOCKIG = 1;
private static final int FRÖHLICH = 2;
//Zahl repräsentiert aktuellen Zustand
private int aktuellerZustand;
//Zustandsabhängiges Verhalten
public void unterhalten(){
if (aktuellerZustand == NEUTRAL){
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hallo!");
} else if (aktuellerZustand == BOCKIG){
//BOCKIG-spezifisches Verhalten...
System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
} else if (aktuellerZustand == FRÖHLICH){
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, mir gehts super!");
}
}
public void kussGeben(){
if (aktuellerZustand == NEUTRAL){
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hihi :-)");
aktuellerZustand = FRÖHLICH; //Zustandsänderung!
} else if (aktuellerZustand == BOCKIG){
//BOCKIG-spezifisches Verhalten...
System.out.println("Na gut! Hab dich wieder lieb.");
aktuellerZustand = NEUTRAL; //Zustandsänderung!
} else if (aktuellerZustand == FRÖHLICH){
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, :-D");
}
}
public void verärgern(){
if (aktuellerZustand == NEUTRAL){
//NEUTRAL-spezifisches Verhalten...
System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
aktuellerZustand = BOCKIG; //Zustandsänderung!
} else if (aktuellerZustand == BOCKIG){
//BOCKIG-spezifisches Verhalten...
System.out.println("Du machst alles bloß noch schlimmer!");
} else if (aktuellerZustand == FRÖHLICH){
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Du spinnst wohl! ;-(");
aktuellerZustand = BOCKIG; //Zustandsänderung!
}
}
}
Es funktioniert zweifellos. Jedoch hat es seinen Grund, dass die reine strukturierte Programmierung durch die Objektorientierung verdrängt wurde:
- Unleserlichkeit und Unübersichtlichkeit. Der Code hat mit unserem Zustandsautomaten kaum etwas gemein. Das Verhalten für jeden Zustand ist über die Methoden verteilt. Man stelle sich nur den Entwurf mit 10 oder mehr Zuständen und 15 Methoden vor. Durch die Verteilung der Zustände sind weiterhin keine expliziten Zustandübergänge vorhanden.
-
Schlechte Wartbarkeit und Erweiterbarkeit. Sollen neue Zustände eingeführt werden, so muss umständlich jede Operation um einen weiteren else-Zweig erweitert werden. Die Fehlerträchtigkeit hierbei ist enorm.
- Wie wäre es mit folgender neuen Anforderung: Um die Modellierung noch realistischer zu machen, soll mit einer Wahrscheinlichkeit von 10% ein Aufruf von unterhalten() im Zustand Neutral zu einem Wechsel in den Zustand Bockig führen, weil man(n) etwas gesagt hat, was der Freundin missfällt. Zurück zur Sache: Es ist leicht vorstellbar, wie schnell solche zahlreichen kleinen und größeren Anforderungen und Fallunterscheidungen in der Summe den eh schon unübersichtlichen Code noch kryptischer machen. Ein Wartungsalptraum.
Durch diese strukturierte Denke verletzen wir zahlreiche OO-Entwurfsprinzipien:
Identifiziere jene Aspekte, die sich ändern und trenne sie von jenen, die konstant bleiben.
Wahrscheinlich müssen in Zukunft neue Zustände integriert werden, die Freundin an sich bleibt dabei jedoch konstant. Bei unserem jetzigem Entwurf muss bei jeder Änderung der Code der Freundin angefasst werden.
Offen/Geschlossen-Prinzip (Open/Closed):
Entwürfe sollten für Erweiterungen offen, aber für Veränderungen geschlossen sein.
Diese Prinzip fordert letztlich das selbe: Der Freundincode soll für Veränderungen geschlossen sein, aber sie soll um neue Zustände jederzeit erweitert werden können.
Kohäsion (= Grad, inwiefern eine Klasse einen einzigen konzentrierten Zweck hat) und Delegation:
Kapsele Verantwortlichkeiten in eigenen Objekten und delegiere Aufrufe an diese Objekte. Es gilt: eine Klasse, eine Verantwortlichkeit.
Gerade das letzte Entwurfsprinzip macht eins deutlich: Die Zustände müssen in eigenen Objekten gekapselt werden.
Schauen wir uns doch noch einmal die Methoden unterhalten(), kussGeben() und verärgern() an. Sie haben alle eine ähnliche Struktur von Bedingungsanweisungen. Nun liegt es Nahe, alle Bedingungszweige, der logisch zu einem Zustand gehört, auch in ein gemeinsames Objekt (das Zustandsobjekt) zu übertragen. Damit enthält jedes entstandene Zustandsobjekt das Verhalten für diesen Zustand.
public void unterhalten() {
if (aktuellerZustand == NEUTRAL) {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hallo!");
}
else if (aktuellerZustand == BOCKIG) {
//BOCKIG-spezifisches Verhalten...
System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
}
else if (aktuellerZustand == FRÖHLICH) {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, mir gehts super!");
}
}
public void kussGeben() {
if (aktuellerZustand == NEUTRAL) {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hihi :-)");
aktuellerZustand = FRÖHLICH; //Zustandsänderung!
}
else if (aktuellerZustand == BOCKIG) {
//BOCKIG-spezifisches Verhalten...
System.out.println("Na gut! Hab dich wieder lieb.");
aktuellerZustand = NEUTRAL; //Zustandsänderung!
}
else if (aktuellerZustand == FRÖHLICH) {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, :-D");
}
}
public void verärgern() {
if (aktuellerZustand == NEUTRAL) {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
aktuellerZustand = BOCKIG; //Zustandsänderung!
}
else if (aktuellerZustand == BOCKIG) {
//BOCKIG-spezifisches Verhalten...
System.out.println("Du machst alles bloß noch schlimmer!");
}
else if (aktuellerZustand == FRÖHLICH) {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Du spinnst wohl! ;-(");
aktuellerZustand = BOCKIG; //Zustandsänderung!
}
}
class Neutral {
public void unterhalten() {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hallo!");
}
public void kussGeben() {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Hihi :-)");
//Zustandsänderung zum Zustand "Fröhlich". Dazu später.
}
public void verärgern() {
//NEUTRAL-spezifisches Verhalten...
System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
//Zustandsänderung zum Zustand "Bockig". Dazu später.
}
}
class Bockig {
public void unterhalten() {
//BOCKIG-spezifisches Verhalten...
System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
}
public void kussGeben() {
//BOCKIG-spezifisches Verhalten...
System.out.println("Na gut! Hab dich wieder lieb.");
//Zustandsänderung zum Zustand "Neutral". Dazu später.
}
public void verärgern() {
//BOCKIG-spezifisches Verhalten...
System.out.println("Du machst alles bloß noch schlimmer!");
}
}
class Fröhlich {
public void unterhalten() {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, mir gehts super!");
}
public void kussGeben() {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Hihi, :-D");
}
public void verärgern() {
//FRÖHLICH-spezifisches Verhalten...
System.out.println("Du spinnst wohl! ;-(");
//Zustandsänderung zum Zustand "Bockig". Dazu später.
}
}
Die Zustandsklassen haben die selbe Schnittstelle, wie die Freundin (unterhalten(), kussGeben(), verärgern()). Die Freundin aggregiert fortan ein solches Zustandsobjekt (= aktueller Zustand) und delegiert Aufrufe an dieses Zustandsobjekt. Dazu wird eine Schnittstelle (hier Interface Zustand) für die Zustände eingeführt.
class Freundin {
//Membervariable mit dem aktuellem Zustand
private Zustand aktuellerZustand;
//Die "neue" Freundin delegiert die Aufruf an ihren aktuellen Zustand
public void unterhalten() {
aktuellerZustand.unterhalten();
}
public void kussGeben() {
aktuellerZustand.kussGeben();
}
public void verärgern() {
aktuellerZustand.verärgern();
}
}
Soweit, so gut. Bleibt nur eine Frage offen: Wie wechselt die Freundin ihre Zustände? Eine denkbar einfache Vorgehensweise ist dabei, das gewünschte Zustandsobjekt bei jedem Zustandswechsel neu zu instanziieren. Dies ist natürlich in unserem Fall (häufige Zustandswechsel) nicht sonderlich klug, jedoch sei es dennoch aus Gründen der Einfachheit und Didaktik realisiert. Die Problematik der Zustandswechsel wird unter Variationen näher diskutiert.
Damit die Zustandsobjekte selbstständig den Zustand der Freundin wechseln können, benötigen sie eine Referenz auf die Freundin. Weiterhin muss die Freundin um einen Setter zum Setzen des gewünschten Zustands erweitert werden und ein mit der Freundin parametrisierter Konstrukur für die Zustände definiert werden, damit die Zustände die Freundin kennen und den aktuellen Zustand der Freundin setzen können.
Der angepasste Entwurf sieht wie folgt aus:
class Freundin {
//Setter zum Setzen des aktuellen Zustands.
public void setAktuellerZustand(Zustand aktuellerZustand){
this.aktuellerZustand = aktuellerZustand;
}
//Defaultzustand Neutral im Konstruktor setzen.
public Freundin(){
setAktuellerZustand(new Neutral(this));
}
//Rest wie gehabt.
private Zustand aktuellerZustand;
public void unterhalten() {
aktuellerZustand.unterhalten();
}
public void kussGeben() {
aktuellerZustand.kussGeben();
}
public void verärgern() {
aktuellerZustand.verärgern();
}
}
interface Zustand{
public void unterhalten();
public void kussGeben();
public void verärgern();
}
class Neutral implements Zustand{
//Konstruktur. Mit Freundin parametrisiert
public Neutral(Freundin freundin){
this.freundin = freundin;
}
//Referenz auf die Freundin
private Freundin freundin;
public void unterhalten() {
System.out.println("Hallo!");
}
public void kussGeben() {
System.out.println("Hihi :-)");
freundin.setAktuellerZustand(new Fröhlich(freundin)); //Zustandsübergang
}
public void verärgern() {
System.out.println("Du spinnst wohl! Ich bin sauer! ;-(");
freundin.setAktuellerZustand(new Bockig(freundin)); //Zustandsübergang
}
}
class Bockig implements Zustand{
//Konstruktur. Mit Freundin parametrisiert
public Bockig(Freundin freundin){
this.freundin = freundin;
}
//Referenz auf die Freundin
private Freundin freundin;
public void unterhalten() {
System.out.println("Fahr jetzt nach Hause! Ich will nicht mit dir reden!");
}
public void kussGeben() {
System.out.println("Na gut! Hab dich wieder lieb.");
freundin.setAktuellerZustand(new Neutral(freundin)); //Zustandsübergang
}
public void verärgern() {
System.out.println("Du machst alles bloß noch schlimmer!");
}
}
class Fröhlich implements Zustand{
//Konstruktur. Mit Freundin parametrisiert
public Fröhlich(Freundin freundin){
this.freundin = freundin;
}
//Referenz auf die Freundin
private Freundin freundin;
public void unterhalten() {
System.out.println("Hihi, mir gehts super!");
}
public void kussGeben() {
System.out.println("Hihi, :-D");
}
public void verärgern() {
System.out.println("Du spinnst wohl! ;-(");
freundin.setAktuellerZustand(new Bockig(freundin)); //Zustandsübergang
}
}
Besonders schön lässt sich unser Entwurf nun durch den Client verwenden. Der Client hat keine Kenntnis von den Zuständen und Zustandswechsel der Freundin. Er kennt allein das Freundinobjekt und interagiert ausschließlich mit diesem. Dabei wechselt die Freundin zur Laufzeit dynamisch ihr Verhalten, ohne dass der Client davon etwas mitbekommt oder gar etwas dazu beiträgt. Er scheint so, als hätte die Freundin ihre Klasse geändert.
Freundin freundin = new Freundin();
//Defaultzustand: Neutral
freundin.unterhalten(); //Hallo!
freundin.verärgern(); //Du spinnst wohl! Ich bin sauer! ;-(
//Ab jetzt: Bockig
freundin.unterhalten(); //Fahr jetzt nach Hause! Ich will nicht mit dir reden!
freundin.unterhalten(); //Fahr jetzt nach Hause! Ich will nicht mit dir reden!
freundin.kussGeben(); //Na gut! Hab dich wieder lieb.
//Ab jetzt: Neutral
freundin.kussGeben(); //Hihi :-)
//Ab jetzt: Fröhlich
freundin.unterhalten(); //Hihi, mir gehts super!
Voila! Unsere neue Freundin kann ihr Verhalten dynamisch in Abhängigkeit von ihrem inneren Zustand verändern. Damit bringt sie zahlreiche Vorzüge mit sich:
- Verständlichkeit und Leserlichkeit. Die Abbildung eines jeden Zustands des Zustandsautomaten auf eine eigene Klasse ist äußerst intuitiv und verständlich.
- Explizite Zustandsübergänge. Ebenso werden die Zustandswechsel explizit durch Setzen einer Variablen (Member "aktuellerZustand" der Freundin) durchgeführt. Dies ist keine kryptische Integer-Variable mehr (bei der die Leserlichkeit nur durch zusätzliche Konstanten nicht vollends verloren geht), sondern ein Zustandsobjekt. Sehr intuitiv und realitätsnahe. Das ist OO.
- Weiterhin werden inkonsistente Zustände verhindert, die hätten auftreten können, wenn der Zustand der Freundin ursprünglich von mehreren Integer-Variablen abhängig gewesen wäre.
- Hohe Kohäsion/Trennung der Verantwortlichkeiten. Das Wissen, um das zustandsspezifische Verhalten ist über mehrere Zustandsklassen aufgeteilt. Die Verantwortlichkeiten sind klar getrennt. Der Code einer jeden Klasse (Freundin, Zustände) wird schlank.
- Hohe Änderungsstabilität und Erweiterbarkeit. Weiterhin führen Änderungen an den Zuständen oder die Integration neuer Zustände lediglich zur geringen Änderungsaufwand. Die Freundinklasse bleibt davon unangetastet (Freundin für Veränderung geschlossen, aber durch neue Zustände erweiterbar -> Offen/Geschlossen-Prinzip). Alles im Allem entsteht eine gute Wartbarkeit des Systems.
- Wiederverwendbarkeit. Die Zustände sind modular und können auch von anderen Objekten neben der Freundin (z. B. Freund, Mutter, Bruder, Dozent, Polizist etc.) verwendet werden.
- Der Entwurf ist (gerade hinsichtlich später Änderungen) weniger fehlerträchtig als breite, sich wiederholende if-else-Konstrukte.
Es zeigt sich, dass durch den Einsatz des State Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgenden Abschnitt das State Design Pattern formalisiert, näher analysiert und diskutiert.
Das Freundinbeispiel mit State Pattern Termini
Klasse | State Teilnehmer |
---|---|
Freundin | Kontext (Context) |
Neutral, Bockig, Fröhlich | Konkrete Zustände (Concrete States) |
Analyse und Diskussion
Gang Of Four-Definition
State:
"Ermögliche es einem Objekt, sein Verhalten zu ändern, wenn sein interner Zustand sich ändert. Es wird so aussehen, als ob das Objekt seine Klasse gewechselt hat."
([GoF], Seite 398)
Beschreibung
Das State Entwurfsmuster ermöglicht die elegante Modellierung von zustandsabhängigen Verhalten eines Objekts. Je nach internen Zustand ändert sich das Verhalten des Objekts.
Es wird eine einheitliche Schnittstelle für die möglichen Zustände definiert (State). Für jeden Zustand wird eine Klasse erstellt, die diese Schnittstelle realisiert (ConcreteStateX).
Das Objekt, dessen Verhalten in Abhängigkeit vom Zustand geändert werden soll (das Context-Objekt) aggregiert nun ein solches Zustandsobjekt (via Instanzvariable). Dieses Objekt repräsentiert den aktuellen internen Zustand und kapselt das zustandsabhängige Verhalten des Contexts. Der Context delegiert Aufrufe an sein aktuell gesetztes Zustandsobjekt.
Die Zustandswechsel und -übergänge können durch die konkreten Zustände selbst durchgeführt werden, in dem sie dem Context einen Folgezustand zu weisen. Mehr dazu unter Variationen.
class Context{
private State state;
public void operate(){
state.operate();
}
}
interface State{
public void operate();
}
class ConcreteState1 implements State{
public void operate() {
System.out.println("ConcreteState1");
//ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
}
}
class ConcreteState2 implements State{
public void operate() {
System.out.println("ConcreteState2");
//ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
}
}
class ConcreteState3 implements State{
public void operate() {
System.out.println("ConcreteState3");
//ggf. Zustandswechsel, dazu Handle auf Context mit entsprechenden Setter notwendig
}
}
Variationen
Definition der Zustandsübergänge
Es muss definiert werden, wo entschieden wird, welcher Zustand als nächstes gesetzt werden soll. Dies kann zu einem in den konkreten Zustandsobjekten, aber auch im Kontextobjekt geschehen.
Zustände bestimmen Folgezustand
Damit jeder Zustand seinen Folgezustand bestimmen kann, muss er diesem beim Contextobjekt setzen. Dazu benötigt er aber
- eine Referenz auf das Contextobjekt.
- Weiterhin muss die Schnittstelle des Contextobjekts um entsprechende Setter erweitert werden.
Damit die Zustandsobjekte nicht immer wieder mit new neu instanziiert werden müssen, kann der Context alle möglichen Zustände als Attribute halten und Zugriff über Getter ermöglichen. Dies ist ein Versuch die Abhängigkeiten zwischen den Zustandsobjekten zu reduzieren.
class Context{
private State currentState;
public void operate(){
currentState.operate();
}
//Setter zum Setzen des aktuellen Zustands
public void setState(State state){
currentState = state;
}
//Context hält alle möglichen Zustände vor
private State concreteState1;
private State concreteState2;
private State concreteState3;
//Getter für die möglichen Zustände
public State getConcreteState1() {
return concreteState1;
}
public State getConcreteState2() {
return concreteState2;
}
public State getConcreteState3() {
return concreteState3;
}
//Initialisierung in Konstruktor
public Context(){
concreteState1 = new ConcreteState1(this);
concreteState2 = new ConcreteState2(this);
concreteState3 = new ConcreteState3(this);
currentState = concreteState1;
}
}
context.setState(context.getConcreteState3());
interface State{
public void operate();
}
class ConcreteState1 implements State{
//Zustand muss Context zum Setzen des Folgezustands kennen.
private final Context context;
//Konstruktor
public ConcreteState1(Context context) {
this.context = context;
}
public void operate() {
System.out.println("ConcreteState1");
//Zustandswechsel, ggf. mit Bedingung
context.setState(context.getConcreteState2());
}
}
class ConcreteState2 implements State{
//Zustand muss Context zum Setzen des Folgezustands kennen.
private final Context context;
//Konstruktor
public ConcreteState2(Context context) {
this.context = context;
}
public void operate() {
System.out.println("ConcreteState2");
//Zustandswechsel, ggf. mit Bedingung
context.setState(context.getConcreteState3());
}
}
class ConcreteState3 implements State{
//Zustand muss Context zum Setzen des Folgezustands kennen.
private final Context context;
//Konstruktor
public ConcreteState3(Context context) {
this.context = context;
}
public void operate() {
System.out.println("ConcreteState3");
//Zustandswechsel, ggf. mit Bedingung
context.setState(context.getConcreteState1());
}
}
Eine Alternative wäre es, die Zustände als Singletons zu realisieren. Damit müsste der Context nicht alle möglichen Zustände und die dafür notwendigen Getter bereitstellen. Dies ist aber nur möglich, wenn die Zustandsobjekte über keine Attribute und Daten verfügen. Eine andere Lösung wäre, die Zustände immer erst bei Bedarf zu erzeugen und danach gleich wieder zu löschen, so dass keine Referenz auf diese gehalten werden muss.
Durch diese Vorgehensweise wird der Entwurf sehr flexibel und dynamisch. Zustandswechsel können beliebig durchgeführt werden. Der Context ist von dieser Verantwortung befreit. Somit können Änderungen an den Zustandsübergängen durchgeführt werden, ohne den Context modifzieren zu müssen.
Context bestimmt Folgezustände
Die Verantwortung für die Zustandsübergänge beim Contextobjekt zu implementieren ist sinnvoll, wenn sich (einmal implementiert) an den Zustandswechsel kaum etwas ändert. Damit werden die Zustände schlanker, da sie sich nicht mehr um die Zustandsübergänge kümmern müssen. Weiterhin werden ihre Abhängigkeiten geringer, da sie das Contextobjekt und andere Zustände nicht mehr kennen müssen. Allerdings ist diese Variante starrer und unflexibler, wenn doch außerordentliche Zustandswechsel notwendig werden.
Folgezustand als Rückgabewert von operate()
Eine besonders interessante Alternative zur Bestimmung der Folgezustände ist folgende: Die operate()-Methode liefert den Folgezustand an den Context zurück. Dadurch kann jeder Zustand seinen Folgezustand selbst bestimmten und der Context ist von dieser Logik entbunden. Dieser Ansatz funktioniert allerdings nur, wenn aus der Programmlogik heraus, die operate()-Methode keinen anderen Rückgabewert haben muss. Ist dies der Fall (z. B. wenn operate() das Ergebnis einer Rechnung zurückliefert), kann dieser Ansatz nicht realisiert werden.
Lebenszeit von Zustandsobjekten
Erzeugung der Zustandsobjekts bei Bedarf | Erzeugung der Zustandsobjekte im Voraus |
---|---|
Zustandsobjekte werden erst bei Bedarf instanziiert und nach erfolgter Nutzung sofort wieder gelöscht.
|
Alle möglichen Zustände werden in Voraus instanziiert und nie gelöscht.
|
Gemeinsame Nutzung von Zustandsobjekten
Wenn die Zustände keinen internen Zustand (also Daten, Membervariablen) haben, so kann zur Einsparung von Ressourcen verschiedenen Contextobjekte die selben Zustandsobjekte nutzen. In diesem Fall könnte man die Realisierung des Singleton Design Pattern für die Zustände in Betracht ziehen.
Anwendungsfälle
Das State Design Pattern findet in folgenden Fällen Anwendung:
- Ein Objekt soll sein äußeres Verhalten zur Laufzeit in Abhängigkeit von seinen Zustand ändern.
-
Ein Objekt besitzt ein Reihe von Methoden ähnlicher Struktur, die sich aus immer gleichen Bedingungsanweisungen zusammensetzen. Die Bedingungsanweisung prüft dabei den Wert einer Membervariablen (Integer, Enumeration), die den aktuellen Zustand repräsentiert.
Objekt mit strukturähnlichen Operationen:public class ObjectWithoutStateDP { //Mögliche "Zustände" private static final int STATE_A = 0; private static final int STATE_B = 1; private static final int STATE_C = 2; //Zahl repräsentiert aktuellen "Zustand" private int currentState; //Methoden ähnlicher Struktur //Achtung: Strukturierte Denke! public void go(){ if (currentState == STATE_A){ //Zustand-A-spezifisches Verhalten... } else if (currentState == STATE_B){ //Zustand-B-spezifisches Verhalten... } else if (currentState == STATE_C){ //Zustand-C-spezifisches Verhalten... } } public void stop(){ if (currentState == STATE_A){ //Zustand-A-spezifisches Verhalten... } else if (currentState == STATE_B){ //Zustand-B-spezifisches Verhalten... } else if (currentState == STATE_C){ //Zustand-C-spezifisches Verhalten... } } public void exit(){ if (currentState == STATE_A){ //Zustand-A-spezifisches Verhalten... } else if (currentState == STATE_B){ //Zustand-B-spezifisches Verhalten... } else if (currentState == STATE_C){ //Zustand-C-spezifisches Verhalten... } } }
Nach dem State Design Pattern wird für jedem Zweig der Bedingungsanweisung, die bei einen bestimmten internen Zustand ausgeführt werden, eine eigene Klasse erstellt - die Zustandsklasse. Alles was noch zu tun ist, ist den Methodenaufruf an eine Instanz dieser Zustandsklasse zu delegieren.
Konkrete Anwendungsfälle:
-
Werkzeuge in Zeichentools. In vielen Zeichentools kann der Benutzer zwischen Werkzeugen wählen. Das Auswahlwerkzeug ermöglicht das Ziehen von Rechtecken auf der Zeichenfläche, um alle Objekte in diesem Bereich auszuwählen. Mit dem Pinselwerkzeug können Striche gemalt werden und das Verlaufswerkzeug ermöglicht schöne Verläufe. Nun erscheint es dem Benutzer so, als würde er ein Werkzeug "packen" und "benutzen". Nun, tatsächlich ändert sich lediglich der Zustand der Zeichenfläche bzw. des Editors. Führte ein Klick in die Zeichenfläche im Zustand "Pinselwerkzeug" noch zur Erstellung eines Punktes, so führt der selbe Klick im Zustand "Füllwerkzeug" zum Ausfüllen der gesamten Zeichenfläche mit einer bestimmten Farbe. Das Verhalten des Editors bzw. seine Reaktion auf Events (Mausklick) ändert sich in Abhängigkeit zu seinen Zustand. Der Zustand wird durch die Auswahl eines Werkzeugs gesetzt.
Jedes Werkzeug setzt den Zustand des Editors -
Zustandsautomaten. Natürlich können alle möglichen Zustandsautomaten (beispielsweise DEAs) mit Hilfe des State Entwurfsmusters elegant abgebildet werden.
Zustandsmaschinen (hier ein DEA/Akzeptor) können mit dem Zustand Entwurfsmuster abgebildet werden.- Parser. Die Funktionsweise vieler Textparser ist zwangsläufig zustandsbasiert. So muss ein Compiler, der Quellcode parst, ein Zeichen abhängig von den zuvor gelesen Zeichen interpretieren. So handelt es sich in vielen Programmiersprachen bei allen Zeichen, die einem "/*" folgen, um Kommentare bis zu einem "*/" (--> Kommentarzustand).
- Repräsentation von Zuständen von (Netzwerk)Verbindungen. [GoF] (Seite 399) beschreibt die Modellierung einer TCP-Verbindung mit den Operationen open(), close(), confirm(). Das Verhalten dieser Methoden ist jedoch abhängig von dem Zustand der TCP-Verbindung: Etabliert, Lauschend oder Geschlossen.
Vorteile
- Erweiterbarkeit und Änderungsstabilität. Neue Zustände können einfach ins System integriert werden, ohne dabei bestehenden Code ändern zu müssen. Änderungen am zustandsabhängigen Verhalten betreffen lediglich eine Zustandsklasse und nicht den Context.
- Intuitivität und Verständlichkeit. Durch die hohe Kohäsion und der Delegation von Verantwortlichkeiten (das Verhalten eines Zustands ist im entsprechenden Zustandsobjekt selbst gekapselt/lokalisiert) wird der Code leicht verständlich. Das mannigfaltige Verhalten eines Objekts wird auf verschiedene Zustandsobjekte verteilt.
- Das äußere Verhalten eines Objekts (des Contexts) kann beliebig geändert werden. Für den Client erscheint es so, als würde er es mit einem neuen Objekt einer anderen Klasse zu tun haben.
- Explizite Zustandsübergänge. Durch die Einführung von Objekten für jeden Zustand wird der Zustandswechsel explizit. Außerdem werden inkonsistente Zustände verhindert, da ein Zustandswechsel ein atomarer Befehl ist (Setzen einer Variablen), im Gegensatz zum Binden mehrerer Membervariablen (Zahlen oder Enumerations) beim strukturierten Ansatz (häufig sind es mehrere).
- Der Entwurf ist (gerade hinsichtlich späterer Änderungen) weniger fehlerträchtig als breite, sich wiederholende if-else-Konstrukte.
Nachteile
- Erhöhte Klassenanzahl.
- Weniger kompakt als eine einzige Klasse. Allerdings ist dies gerade das Ziel des Entwurfsmusters: Die Aufteilung des Verhaltens von einer einzigen unübersichtlichen Klasse auf mehrere Zustandsobjekte.
Exkurs: Vergleich zwischen State und Strategy Design Pattern
Beim Vergleich des State mit dem Strategy Design Pattern fällt eins sofort auf: beide haben ein identisches Klassendiagramm! Dennoch gibt es große Unterschiede in der Verwendung und der Absicht, die hinter den Pattern stecken, obwohl sie die gleiche Struktur haben.
State | Strategy |
---|---|
Gemeinsamkeit: Beide Pattern kapselt Verhalten in einem separaten Objekt. Der Context delegiert Aufrufe an dieses Objekt. Damit kann das Verhalten des Objekts flexibel (und auch zur Laufzeit) durch Setzen eines anderen Verhaltensobjekts geändert werden. | |
|
|
Ergo: Gleiche Struktur, aber unterschiedliche Absicht. |