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.