Muster: State

Aus Wikibooks

State / Zustand[Bearbeiten]

Der Zustand (engl. state) wird verwendet, um Änderungen des Verhaltens eines Objekts abhängig von seinem Zustand zu ermöglichen. Auch bekannt als Objekte für Zustände (objects for states).

Verwendung[Bearbeiten]

  • Das Verhalten eines Objekts ist abhängig von seinem Zustand.
  • 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[Bearbeiten]

Zustandsautomat[Bearbeiten]

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

Hierarchische Zustandsautomat[Bearbeiten]

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[Bearbeiten]

Einfache Zustände[Bearbeiten]

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 das ursprüngliche Objekt die separaten Zustandsklassen einheitlich behandeln kann, wird eine gemeinsame Abstrahierung dieser Klassen definiert. Bei einem Zustandsübergang tauscht das ursprüngliche Objekt das von ihm verwendete Zustandsobjekt aus.

Klassendiagramm
Klassendiagramm

Implementierung[Bearbeiten]

Für das Ausprogrammieren des Zustandsmusters sollte vorher überlegt werden, ob

  • der Kontext die Zustände als Attribute hält.
  • bei jedem Zustandswechsel der vorherige Zustand gelöscht und ein neuer Zustand instantiiert wird, was zwar rechentechnisch aufwendiger aber sauberer für den Programmablauf ist. Das heißt, dass alle Membervariablen eines jeden Zustands gelöscht werden und ein späteres Debuggen von Anfang an ausgeschlossen wird. Man kann dann auch diesen Zustand mehrmals in ähnlichen Kontexten verwenden. (Z. B. können zwei Dateien eines Dateisystems zur gleichen Zeit geöffnet sein. Dabei befinden sich beide Dateien im Zustand Open. Vom Open-Zustand existieren daher zwei Instanzen, jeweils eine Instanz für jede Datei.)
  • jeder Zustand nur einmal erzeugt wird - dann wenn er zum ersten Mal gebraucht wird - und daher alle möglichen Zustände parallel im Speicher existieren (hier wird das Singleton-Erzeugermuster verwendet).

Akteure[Bearbeiten]

  • Kontext
    • Definiert die klientseitige Schnittstelle.
    • Verwaltet die separaten Zustandsklassen und tauscht diese bei einem Zustandsübergang aus.
  • (Abstrakter) Zustand
    • Definiert eine einheitliche Schnittstelle aller Zustandsobjekte.
    • Implementiert gegebenenfalls ein Standardverhalten. Beispielsweise kann in der abstrakten Zustandsklasse die Ausführung jeglichen Verhaltens gesperrt werden. Ein bestimmtes Verhalten kann dann nur ausgeführt werden, wenn es vom konkreten Zustand durch Überschreiben der entsprechenden Methode freigeschaltet wurde.
  • Konkreter Zustand
    • Implementiert das Verhalten, das mit dem Zustand des Kontextobjekts verbunden ist.

Vorteile[Bearbeiten]

  • Komplexe und schwer leserliche Bedingungsanweisungen können vermieden werden.
  • Neue Zustände und neues Verhalten können auf einfache Weise hinzugefügt werden. Die Wartbarkeit wird erhöht.
  • Zustandobjekte können wiederverwendet werden.

Nachteile[Bearbeiten]

  • Bei sehr einfachem zustandsbehaftetem Verhalten rechtfertigt unter Umständen der Nutzen den Implementierungsaufwand nicht.

Beispiele[Bearbeiten]

Prinzipiell kann jedes zustandsabhängige Verhalten durch dieses Entwurfsmuster abgebildet werden. Einige Beispiele für zustandsbehaftete Problemstellungen sind

  • Verwaltung von Sessions
  • Verwaltung von Ein- und Ausgabeströmen
  • Zustandbehaftete Bedienelemente einer grafischen Benutzeroberfläche
  • Parkautomaten

Pseudokode[Bearbeiten]

Als Beispiel nehmen wir ein Zeichnungsprogramm mit einem Mauszeiger, dass zu jedem Zeitpunkt als eines von mehreren Werkzeugen agieren kann. Statt zwischen mehreren Zeigerobjekten zu wechseln behält der Cursor einen internen Zustand, der das derzeit verwendete Werkzeug representiert. Wird eine werkzeugabhängige Methode wie z. B. als ein Resultat eines Mausklicks aufgerufen, wird der Methodenaufruf dem Cursorzustand übergeben.

Jedes Werkzeug entspricht einem Zustand. Die geteilte abstrakte Klasse heißt AbstractTool.

 class AbstractTool is
     function moveTo(point) is
         input:  Die Position point, wohin die Maus bewegt wurde
         (Diese Funktion muss von Unterklassen implementiert werden.)
 
     function mouseDown(point) is
         input:  Die Position point, wo die Maus ist
         (Diese Funktion muss von Unterklassen implementiert werden.)
 
     function mouseUp(point) is
         input:  Die Position point, wo die Maus ist
         (Diese Funktion muss von Unterklassen implementiert werden.)

Gemäß dieser Definition muss jedes Werkzeug die Cursorbewegung behandeln und auch den Anfang und das Ende jedes Klicks oder Verschiebens.

Mit dieser Basisklasse sehen der einfache Stift und die Auswahlwerkzeuge wie folgt aus:

 subclass PenTool of AbstractTool is
     last_mouse_position := invalid
     mouse_button := up
 
     function moveTo(point) is
         input:  Die Position point, wohin die Maus bewegt wurde
         if mouse_button = down
             (draw a line from the last_mouse_position to point)
             last_mouse_position := point
 
     function mouseDown(point) is
         input:  Die Position point, wo die Maus ist
         mouse_button := down
         last_mouse_position := point
 
     function mouseUp(point) is
         input:  Die Position point, wo die Maus ist
         mouse_button := up  
 subclass SelectionTool of AbstractTool is
     selection_start := invalid
     mouse_button := up
 
     function moveTo(point) is
         input:  Die Position point, wohin die Maus bewegt wurde
         if mouse_button = down
             (select the rectangle between selection_start and point)
 
     function mouseDown(point) is
         input:  Die Position point, wo die Maus ist
         mouse_button := down
         selection_start := point
 
     function mouseUp(point) is
         input:  Die Position point, wo die Maus ist
         mouse_button := up

Für dieses Beispiel wurde die Klasse für den Kontext Cursor benannt. Die Methoden in der abstrakten Zustandsklasse heißen (AbstractTool in diesem Falle) werden auch im Kontext implementiert. Die Kontextklasse rufen diese Methoden die entsprechenden Methoden des derzeitigen Zustands auf, die durch current_tool dargestellt werden.

 class Cursor is
     current_tool := new PenTool
 
     function moveTo(point) is
         input:  Die Position point, wohin die Maus bewegt wurde
         current_tool.moveTo(point)
 
     function mouseDown(point) is
         input:  Die Position point, wo die Maus ist
         current_tool.mouseDown(point)
 
     function mouseUp(point) is
         input:  Die Position point, wo die Maus ist
         current_tool.mouseUp(point)
 
     function usePenTool() is
         current_tool := new PenTool
 
     function useSelectionTool() is
         current_tool := new SelectionTool

Beachten Sie, wie ein Cursor-Objekt sowohl als PenTool als auch als SelectionTool an unterschiedlichen Punkten agieren kann, indem es die passende Methodenaufrufe entsprechend dem gerade aktiven Werkzeug übergibt. Dies ist das Grundlegende des Zustands (state pattern). In diesem Falle könnte man das Objekt mit dem Zustand kombinieren, indem man eine PenCursor- und eine SelectCursor-Klasse kreiert, wodurch die Lösung zu einer einfachen "Vererbungslösung" werden würde. Aber in der Praxis könnte Cursor Daten beinhalten, die bei jeder neuen Auswahl eines Werkzeugs zu aufwendig oder unelegant zu kopieren wären.

Java[Bearbeiten]

Hier ist ein Beispiel für das Verhaltensmuster Zustand:

/**
 * Class that comprises of constant values and recurring algorithms.
 */
public class Common {

    /**
     * Changes the first letter of the string to upper case
     * @param WORDS
     * @return Requested value
     */
    public static String firstLetterToUpper(final String WORDS) {
        String firstLetter = "";
        String restOfString = "";
        
        if (WORDS != null) {
            char[] letters = new char[1];
            
            letters[0] = WORDS.charAt(0);
            firstLetter = new String(letters).toUpperCase();
            restOfString = WORDS.toLowerCase().substring(1);
        }
        
        return firstLetter + restOfString;
    }
}

interface Statelike {

    /**
     * Writer method for the state name.
     * @param STATE_CONTEXT
     * @param NAME
     */
    void writeName(final StateContext STATE_CONTEXT, final String NAME);
    
}

class StateA implements Statelike {
    /* (non-Javadoc)
     * @see state.Statelike#writeName(state.StateContext, java.lang.String)
     */
    @Override
    public void writeName(final StateContext STATE_CONTEXT, final String NAME) {
        System.out.println(Common.firstLetterToUpper(NAME));
        STATE_CONTEXT.setState(new StateB());
    }

}

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

    /* (non-Javadoc)
     * @see state.Statelike#writeName(state.StateContext, java.lang.String)
     */
    @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;
        /**
         * Standard constructor
         */
    public StateContext() {
        setState(new StateA());
    }

        /**
         * Setter method for the state.
         * Normally only called by classes implementing the State interface.
         * @param NEW_STATE
         */
    public void setState(final Statelike NEW_STATE) {
        myState = NEW_STATE;
    }

        /**
         * Writer method
         * @param NAME
         */
    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äß dem Code oben ist die Ausgabe der main()-Methode von TestClientState:

Montag
DIENSTAG
MITTWOCH
Donnerstag
FREITAG
SAMSTAG
Sonntag