Zustand (Entwurfsmuster)

Der Zustand (englisch state) ist ein Entwurfsmuster aus dem Bereich der Softwareentwicklung, das zur Kategorie der Verhaltensmuster (englisch behavioral design patterns) gehört. Das Zustandsmuster wird zur Kapselung unterschiedlicher, zustandsabhängiger Verhaltensweisen eines Objektes eingesetzt.[1]

Das Zustandsmuster ist eines der sogenannten „GoF“-Muster, d. h., es ist eines der im Buch Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software aufgeführten Entwurfsmuster („GoF“ steht für „Gang of Four“ oder „Viererbande“ nach den vier Autoren dieses 1994 veröffentlichten Buches). Das Zustandsmuster ist auch bekannt als „Objekte für Zustände“ (objects for states).

Verwendung

Grundsätzlich gilt, dass das Verhalten eines Objekts abhängig von seinem Zustand ist. Durch die übliche Implementierung soll vermieden werden, die Zustände eines Objekts und das davon abhängige Verhalten in einer großen switch-Anweisung (basierend auf enumerierten Konstanten) zu kodieren. Jeder Fall der switch-Anweisung soll in einer eigenen Klasse implementiert werden, so dass der Zustand des Objektes selbst wieder ein Objekt ist, das unabhängig von anderen Objekten ist.

Problem

Zustandsautomat

Für ein Objekt sind verschiedene Zustände, die möglichen Übergänge zwischen diesen Zuständen und das davon abhängige Verhalten zu definieren. Dies ist hier in Form eines endlichen Automaten dargestellt. Dabei zeigt der schwarze Kreis auf den Startzustand und der schwarze Kreis mit der weißen Umrandung auf den Endzustand. Die gerichteten Kanten (Pfeile) zwischen den Zuständen Closed, Open und Deleted definieren den Zustandswechsel.

Zustandsdiagramm
Zustandsdiagramm

Hierarchischer Zustandsautomat

Ein einzelner Zustand eines Objektes kann wiederum in eine Anzahl verschiedener Zustände aufgeteilt werden. Den Zustand Open kann man beispielsweise unterteilen in Read und Write. Sie bilden einen zusammengesetzten Zustand Open. Closed sowie Deleted betrachtet man unabhängig vom zusammengesetzten Zustand Open. Diese Zustände kann man in einer Hierarchie anordnen. Open, Closed und Deleted sind in der ersten Ebene. In der zweiten Ebene befinden sich Read und Write, die dem Zustand Open zugeordnet sind.

Lösung

Einfache Zustände

Das zustandsabhängige Verhalten des Objekts wird in separate Klassen ausgelagert, wobei für jeden möglichen Zustand eine eigene Klasse eingeführt wird, die das Verhalten des Objekts in diesem Zustand definiert. Damit der Kontext die separaten Zustandsklassen einheitlich behandeln kann, wird eine gemeinsame Abstrahierung dieser Klassen definiert.

Bei einem Zustandsübergang tauscht der Kontext das von ihm verwendete Zustandsobjekt aus.

Klassendiagramm
Klassendiagramm

Akteure

Im Entwurfsmuster des Zustandes spielen drei Akteure eine Rolle. Der Kontext definiert die clientseitige Schnittstelle und verwaltet die separaten Zustandsklassen. Außerdem tauscht er diese bei einem Zustandsübergang aus.

Der Zustand definiert eine einheitliche Schnittstelle aller Zustandsobjekte und implementiert gegebenenfalls ein Standardverhalten. Beispielsweise kann im abstrakten Zustand die Ausführung jeglichen Verhaltens gesperrt werden. Das Verhalten kann in diesem Falle nur dann ausgeführt werden, wenn es vom konkreten Zustand durch Überschreiben der entsprechenden Methode freigeschaltet wurde.

Der konkrete Zustand wiederum implementiert das Verhalten, das mit dem Zustand des Kontextobjektes verbunden ist.

Varianten

  • Für den Akteur Zustand kann eine Schnittstelle anstatt einer abstrakten Klasse definiert werden.
  • Können mehrere Kontexte die gleichen Statusobjekte verwenden (sofern die Status durch ihre jeweiligen Klassen und nicht durch Instanzen definierbar sind bzw. Eigenschaftswerte in den Kontext ausgelagert werden können [z. B. der Dateiname]), kann Speicherplatz eingespart werden.

Vor- und Nachteile

Ein Vorteil des Systems ist, dass komplexe und schwer zu lesende Bedingungsanweisungen vermieden werden können. Außerdem können neue Zustände und neues Verhalten auf einfache Weise hinzugefügt werden. Die Wartbarkeit wird erhöht und Zustandsobjekte können wiederverwendet werden.

Auf der anderen Seite rechtfertigt der Nutzen bei sehr einfachem zustandsbehaftetem Verhalten unter Umständen nicht den teils beträchtlichen Implementierungsaufwand. Kann das Objekt sehr viele Zustände annehmen, in denen jeweils nur sehr wenige Aktionen erlaubt sind, muss dennoch jeder Zustand für jede Aktion der anderen Zustände Code enthalten, um die Schnittstelle korrekt zu implementieren, auch wenn in diesen jeweils nur eine Ausnahmebehandlung stattfindet. In einer großen Bedingungsanweisung ließe sich die Ausnahmebehandlung hingegen in einem gemeinsamen "sonst"-Zweig vereinen.

Beispiele

Prinzipiell kann jedes zustandsabhängige Verhalten durch dieses Entwurfsmuster abgebildet werden. Beispielsweise wird es für die Verwaltung von Sessions oder von Ein- und Ausgabeströmen, bei zustandbehafteten Bedienelementen einer grafischen Benutzeroberfläche oder bei Parkautomaten verwendet.

C++

Diese C++14 Implementierung basiert auf dem vor C++98 Beispielcode im Buch Entwurfsmuster.

#include <iostream>
#include <stdexcept>
#include <memory>

typedef const char TCPOktettStream;
class TCPZustand;

// definiert die Klienten interessierende Schnittstelle.
class TCPVerbindung { // Kontext
public:
  TCPVerbindung();
  void oeffneAktiv();
  void oeffnePassiv();
  void synchronisiere();
  void sende(TCPOktettStream*);
  void schliesse();
  void bearbeiteOktett(TCPOktettStream* o) {
    std::cout << ".." << this << " TCPVerbindung::bearbeiteOktett " << o << '\n';
  }
private:
  friend class TCPZustand;
  void aendereZustand(TCPZustand* s) {
    zustand = s; // braucht TCPZustand Deklaration
  }
private:
  // verwaltet eine Instanz (Exemplar) einer KonkreterZustand-Unterklasse, welche den aktuellen Zustand definiert.
  TCPZustand* zustand;
};

// definiert eine Schnittstelle zur Kapselung des mit einem bestimmten Zustand des Kontextobjekts verbundenen Verhaltens.
class TCPZustand { // Zustand
public:
  virtual void uebertrage(TCPVerbindung*, TCPOktettStream*) = 0;
  virtual void oeffneAktiv(TCPVerbindung*) = 0;
  virtual void oeffnePassiv(TCPVerbindung*) = 0;
  virtual void schliesse(TCPVerbindung*) = 0;
  virtual void synchronisiere(TCPVerbindung*) = 0;
  virtual ~TCPZustand() = default;
protected:
  void aendereZustand(TCPVerbindung* t, TCPZustand* s) {
    std::cout << "..TCPZustand::aendereZustand zu " << s << '\n';
    t->aendereZustand(s);
  }
};

void TCPVerbindung::oeffneAktiv() {
  std::cout << this << " TCPVerbindung::oeffneAktiv\n";
  zustand->oeffneAktiv(this); // braucht TCPZustand Definition
}
void TCPVerbindung::oeffnePassiv() {
  std::cout << this << " TCPVerbindung::oeffnePassiv\n";
  zustand->oeffnePassiv(this);
}
void TCPVerbindung::sende(TCPOktettStream* s) {
  std::cout << this << " TCPVerbindung::sende " << s << '\n';
  zustand->uebertrage(this, s);
}
void TCPVerbindung::synchronisiere() {
  std::cout << this << " TCPVerbindung::synchronisiere\n";
  zustand->synchronisiere(this);
}
void TCPVerbindung::schliesse() {
  std::cout << this << " TCPVerbindung::schliesse\n";
  zustand->schliesse(this);
}

// jede Unterklasse implementiert ein Verhalten, das mit einem Zustand des Kontextobjekts verbunden ist.
class TCPEtabliert : public TCPZustand { // KonkreterZustand
public:
  static TCPZustand* instance();
  virtual void oeffneAktiv(TCPVerbindung*) override {
    throw std::runtime_error("TCPEtabliert::oeffneAktiv");
  }
  virtual void oeffnePassiv(TCPVerbindung*) override {
    throw std::runtime_error("TCPEtabliert::oeffnePassiv");
  }
  virtual void synchronisiere(TCPVerbindung*) override {
    throw std::runtime_error("TCPEtabliert::synchronisiere");
  }
  virtual void uebertrage(TCPVerbindung*, TCPOktettStream*) override;
  virtual void schliesse(TCPVerbindung*) override;
};

class TCPBereit : public TCPZustand { // KonkreterZustand
public:
  static TCPZustand* instance();
  virtual void oeffneAktiv(TCPVerbindung*) override {
    throw std::runtime_error("TCPBereit::oeffneAktiv");
  }
  virtual void oeffnePassiv(TCPVerbindung*) override {
    throw std::runtime_error("TCPBereit::oeffnePassiv");
  }
  virtual void synchronisiere(TCPVerbindung*) override;
  virtual void uebertrage(TCPVerbindung*, TCPOktettStream*) override {
    throw std::runtime_error("TCPBereit::uebertrage");
  }
  virtual void schliesse(TCPVerbindung*) override {
    throw std::runtime_error("TCPBereit::schliesse");
  }
};

class TCPBeendet : public TCPZustand { // KonkreterZustand
public:
  static TCPZustand* instance();
  virtual void oeffneAktiv(TCPVerbindung*) override;
  virtual void oeffnePassiv(TCPVerbindung*) override;
  virtual void synchronisiere(TCPVerbindung*) override {
    throw std::runtime_error("TCPBeendet::synchronisiere");
  }
  virtual void uebertrage(TCPVerbindung*, TCPOktettStream*) override {
    throw std::runtime_error("TCPBeendet::uebertrage");
  }
  virtual void schliesse(TCPVerbindung*) override {
    throw std::runtime_error("TCPBeendet::schliesse");
  }
};

// braucht TCPBeendet definition
TCPVerbindung::TCPVerbindung() :zustand(TCPBeendet::instance()) {}

// Unterklassen von TCPZustand besitzen keinen lokalen Zustand, so daß sie gemeinsam genutzt werden können und nur eine Instanz (Exemplar) benötigt wird.
static TCPEtabliert tcpEtabliert;
static TCPBereit tcpBereit;
static TCPBeendet tcpBeendet;

TCPZustand* TCPEtabliert::instance() {
  return &tcpEtabliert;
}
TCPZustand* TCPBereit::instance() {
  return &tcpBereit;
}
TCPZustand* TCPBeendet::instance() {
  return &tcpBeendet;
}

// gültige Methoden der TCPZustand Kindklassen 
void TCPEtabliert::uebertrage(TCPVerbindung* t, TCPOktettStream* o) {
  std::cout << ".TCPEtabliert::uebertrage " << o << '\n';
  t->bearbeiteOktett(o);
}
void TCPEtabliert::schliesse(TCPVerbindung* t) {
  std::cout << ".TCPEtabliert::schliesse\n";
  aendereZustand(t, TCPBeendet::instance());
}
void TCPBereit::synchronisiere(TCPVerbindung* t) {
  std::cout << ".TCPBereit::synchronisiere\n";
  aendereZustand(t, TCPEtabliert::instance());
}
void TCPBeendet::oeffneAktiv(TCPVerbindung* t) {
  std::cout << ".TCPBeendet::oeffneAktiv\n";
  aendereZustand(t, TCPEtabliert::instance());
}
void TCPBeendet::oeffnePassiv(TCPVerbindung* t) {
  std::cout << ".TCPBeendet::oeffnePassiv\n";
  aendereZustand(t, TCPBereit::instance());
}

int main() {
  // Die Smart Pointers verhindern Memory Leaks
  std::unique_ptr<TCPVerbindung> tc1 = std::make_unique<TCPVerbindung>();
  std::unique_ptr<TCPVerbindung> tc2 = std::make_unique<TCPVerbindung>();

  tc1->oeffneAktiv();
  tc2->oeffnePassiv();
  tc1->sende("Hello, ");
  tc2->synchronisiere();
  tc2->sende("world");
  tc1->schliesse();  
  tc2->schliesse();
  tc2->sende("again");
}

Die Programmausgabe ist ähnlich zu:

0x19d9e70 TCPVerbindung::oeffneAktiv
.TCPBeendet::oeffneAktiv
..TCPZustand::aendereZustand zu 0x6030a8
0x19d9e90 TCPVerbindung::oeffnePassiv
.TCPBeendet::oeffnePassiv
..TCPZustand::aendereZustand zu 0x6030b0
0x19d9e70 TCPVerbindung::sende Hello, 
.TCPEtabliert::uebertrage Hello, 
..0x19d9e70 TCPVerbindung::bearbeiteOktett Hello, 
0x19d9e90 TCPVerbindung::synchronisiere
.TCPBereit::synchronisiere
..TCPZustand::aendereZustand zu 0x6030a8
0x19d9e90 TCPVerbindung::sende world
.TCPEtabliert::uebertrage world
..0x19d9e90 TCPVerbindung::bearbeiteOktett world
0x19d9e70 TCPVerbindung::schliesse
.TCPEtabliert::schliesse
..TCPZustand::aendereZustand zu 0x6030b8
0x19d9e90 TCPVerbindung::schliesse
.TCPEtabliert::schliesse
..TCPZustand::aendereZustand zu 0x6030b8
0x19d9e90 TCPVerbindung::sende again
terminate called after throwing an instance of 'std::runtime_error'
  what():  TCPBeendet::uebertrage

Java

Hier ist ein Beispiel für das Verhaltensmuster Zustand:

interface Statelike {

    void writeName(final StateContext STATE_CONTEXT, final String NAME);
}

class StateA implements Statelike {

    @Override
    public void writeName(final StateContext STATE_CONTEXT, final String NAME) {
        System.out.println(NAME.toLowerCase());
        STATE_CONTEXT.setState(new StateB());
    }
}

class StateB implements Statelike {
    /** State counter */
    private int count = 0;

    @Override
    public void writeName(final StateContext STATE_CONTEXT, final String NAME) {
        System.out.println(NAME.toUpperCase());
        // Change state after StateB's writeName() gets invoked twice
        if(++count > 1) {
            STATE_CONTEXT.setState(new StateA());
        }
    }
}

Die Kontextklasse hat eine Zustandsvariable, die sie hier als StateA in einem Anfangszustand instanziiert. In seinen Methoden verwendet sie die entsprechenden Methoden des Zustandsobjekts.

public class StateContext {
    private Statelike myState;

    public StateContext() {
        setState(new StateA());
    }

    public void setState(final Statelike NEW_STATE) {
        myState = NEW_STATE;
    }

    public void writeName(final String NAME) {
        myState.writeName(this, NAME);
    }
}

Der Test unten soll auch die Verwendung veranschaulichen:

public class TestClientState {
    public static void main(String[] args) {
        final StateContext SC = new StateContext();

        SC.writeName("Montag");
        SC.writeName("Dienstag");
        SC.writeName("Mittwoch");
        SC.writeName("Donnerstag");
        SC.writeName("Freitag");
        SC.writeName("Samstag");
        SC.writeName("Sonntag");
    }
}

Gemäß obigem Code ist die Ausgabe der main()-Methode von TestClientState:

montag
DIENSTAG
MITTWOCH
donnerstag
FREITAG
SAMSTAG
sonntag

Weblinks

Wikibooks: Zustand – Implementierung des Zustands in unterschiedlichen Sprachen

Einzelnachweise

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. 5. Auflage. Addison-Wesley, 1996, ISBN 3-8273-1862-9, S. 398 (englisch: Design Patterns. Elements of Reusable Object-Oriented Software.).

Auf dieser Seite verwendete Medien

StatePattern Classdiagramm.png
Autor/Urheber:

Michael Täge

, Lizenz: CC-BY-SA-3.0

Klassendiagramm des Zustand-Entwurfsmusters

StatePattern Statechart.png
Autor/Urheber:

Michael Täge

, Lizenz: CC-BY-SA-3.0

Zustandsdiagramm des Zustand-Entwurfsmusters