Implementierung einer einfachen Zustandsmaschine (state machine) in Java

JSE

Florian Miess

Manchmal möchte man in seinem Programm eine simple Zustandsmaschine nutzen, wobei man nicht auf bestehende Lösungen zurückgreifen möchte - weil sie zu unflexibel oder zu groß sind. Eine solche Zustandsmaschine kann dann auf verschiedenen Arten implementiert werden - häufig werden dabei verschachtelte Switch-Blöcke verwendet. Das vorliegende Beispiel zeigt eine andere, sinnvollere Implementierung auf Basis einer Schleife, die über Zustand-Übergangs-Klassen iteriert.

“State machine Beispiel” zeigt ein Beispiel einer solchen Zustandsmaschine, die Zustandsübergänge für ein Request-Objekt kontrollieren und anwenden soll:

State machine Beispiel
State machine Beispiel

Für unsere simple Zustandsmaschine implementieren wir - von Hilfsklassen abgesehen - nun drei Klassen:

  1. Die Zustandsmaschine
  2. Eine Klasse mit der Konfiguration
  3. Das eigentliche Request-Objekt

In der Klasse “StateMachine” steckt die ganze Logik: Hier wird schlicht über die hinterlegte Konfiguration iteriert. Wenn der gewünschte Zustandsübergang gefunden wurde, wird die entsprechende Status-Transition durchgeführt.

(Als Verbesserung könnte auch eine entspreche Fehlermeldung geworfen werden, sollte die gewünschte Transition nicht möglich sein.)

public class StateMachine {
    private List<Transition> transitions;
    private Request request;

    public static StateMachine build(TransitionConfiguration config, 
                                     Request request) {
        return new StateMachine(config, request);
    }

    private StateMachine(TransitionConfiguration config, Request request) {
        this.request = request;
        this.transitions = config.getTransitions();
    }

    // Apply a new action to the current request
    public StateMachine apply(Action action) {
        for (Transition transition : transitions) {
            boolean currentStateMatch = transition.from
                                                  .equals(request.getState());
            boolean conditionsMatch = transition.action
                                                .equals(action);
            
            if (currentStateMatch && conditionsMatch) {
                System.out.println("Applying " + transition.to);
            
                request.setState(transition.to);
                break;
            }
        }
        return this;
    }
}

Die Konfiguration der Zustandsübergänge ist im “TransitionConfiguration” enthalten:

public enum TransitionConfiguration {
    DEFAULT(Lists.newArrayList(
        new Transition(State.DRAFT, 
                       Action.SUBMIT_REQUEST, 
                       State.IN_APPROVAL),
        new Transition(State.IN_CHANGE, 
                       Action.RESUBMIT_REQUEST, 
                       State.IN_APPROVAL),
        new Transition(State.IN_APPROVAL, 
                       Action.APPROVE_REQUEST, 
                       State.APPROVED), 
        new Transition(State.IN_APPROVAL, 
                       Action.ASK_FOR_CHANGE, 
                       State.IN_CHANGE)
    ));

    private List<Transition> transitions;

    TransitionConfiguration(List<Transition> transitions) {
        this.transitions = transitions;
    }

    public List<Transition> getTransitions() {
        return transitions;
    }
}

Das Code-Beispiel “Request” zeigt das Request-Objekt, das seinen aktuellen Status selbst vorhält:

public class Request {
    private String objectName;
    private State state;

    public Request(String objectName, State currentState) {
        this.objectName = objectName;
        this.state = currentState; 
    }
    // setter/getter...
}

Das vollständige Beispiel ist hier zu finden.

Die Vorteile dieser Implementierung sind vielfältig:

  1. Es wird keine separate Entität benötigt, um den aktuellen Objekt-Zustand zu speichern, der Zustand ist im Objekt (Request) selbst enthalten und kann mit diesem persistiert werden.
  2. Die Konfiguration der Status-Übergänge ist als Programmcode vorhanden. Damit können diese einfach in Unit Tests überprüft werden. Außerdem werden Fehler schon zur Compile-Zeit sichbar.
  3. Eine Erweiterung der Statemachine ist einfach möglich. Z.B. könnte die gewünschte Transition geprüft werden oder das Transition-Objekt könnte weitere Eigenschaft wie Vorbedigungen/Nachbedigungen enthalten, die die Statemachine ausführt.
  4. Die Zustandsmaschine muß kein Wissen über die möglichen Übergänge/Aktionen etc. besitzen.
  5. Damit sind keine ewig langen switch-Anweisungen für die Statusübergänge nötig.
  6. Die Zustandsmaschine kann sehr simpel gehalten werden

Ein anderes Beispiel für das Prinzip dieser Implementierung könnte eine Permission-Machine sein, die anstatt von Transitionen eine Liste von Permissions erhält. Dort wiederum könnte man Operation auf USer oder Domänen abbilden, die von der PermissionMachine überprüft werden.

Thumb 491788583322 1362829742

Florian Miess arbeitet als Senior Consultant bei der innoQ Deutschland GmbH.

More content

Comments

Please accept our cookie agreement to see full comments functionality. Read more