Simpele StateMachine met annotaties

 
16 juli 2008

Een tijdje geleden was ik op zoek naar leuke en interessante voorbeelden waarin annotaties gebruikt werden. Ik kwam terecht op een blog van Gabriele Carcassi over een state machine geschreven met annotaties. Het idee is mooi, maar overbodig complex.
Het leek mij leuk om ook een poging te ondernemen, maar dan op een eenvoudigere manier!

Over het algemeen maak ik gebruik van een Enum om de state van een object in bij te houden. Zoiets als dit:

public enum Switch {
  ON,
  OFF
}

Nu wilde ik graag een Enum definiëren met annotaties. Het vorige voorbeeld heb ik daarom uitgebreid tot:

public enum Switch {
  @Transitions("OFF")
  ON,

  @Start
  @Transitions("ON")
  OFF
}

De volgende regels heb ik erbij bedacht:

  • Start, om aan te geven dat dit de start status is en waarmee het object tevens geïnitialiseerd wordt.
  • Transitions, om aan te geven wat een mogelijke transitie is van de geannoteerde status.

Gezien deze regels is de initiële status ‘OFF’. Er kan van ‘OFF’ naar de status ‘ON’ geswitcht worden en van ‘ON’ weer terug naar ‘OFF’

Na een uitgebreide zoektocht naar een mooie oplossing heb ik uiteindelijk besloten om het met een abstracte basis class op te lossen. Deze is uiteraard wel voorzien van generics om het mooi generiek te maken. De uiteindelijke class is als volgt:

public abstract class Workflow<T extends Enum> {

  private static void checkEnumeratedType(Enum enumType) {
    Field[] fields = enumType.getClass().getFields();
    if (fields == null || fields.length == 0) {
      throw new InvalidStateEnumerationException("Enumerated type does not contain any annotated fields");
    }

    boolean hasStart = false;

    for (Field field : fields) {
      if (field.isAnnotationPresent(Start.class)) {
        hasStart = true;
      }
      if (!field.isAnnotationPresent(Transitions.class)) {
        throw new InvalidStateEnumerationException("Missing @Transitions on enumerated field: " + enumType);
      }
   }

    if (!hasStart) {
      throw new InvalidStateEnumerationException("State enumeration has no start state!");
    }
  }

  private T currentState;

  public Workflow(T initialState) {
    super();
    checkEnumeratedType(initialState);
    setCurrentState(initialState);
  }

  protected boolean canChangeStateTo(T newState) {
    Field currentStateField = getFieldOfEnum(getCurrentState());
    Transitions transitionsAnnotation = currentStateField.getAnnotation(Transitions.class);
    for (String nextAllowedState : transitionsAnnotation.value()) {
      if (nextAllowedState.equals(newState.name())) {
        return true;
      }
    }
    return false;
  }

  protected void changeStateTo(T newState) throws IllegalStateChangeException {
    if (canChangeStateTo(newState)) {
      setCurrentState(newState);
    }
    else {
      throw new IllegalStateChangeException(String.format("State change not allowed '%1$s' -> '%2$s'", getCurrentState(), newState));
    }
  }

  public T getCurrentState() {
    return currentState;
  }

  private Field getFieldOfEnum(T enumType) {
    try {
      return enumType.getClass().getField(enumType.name());
    }
    catch (Exception e) {
      throw new InvalidStateEnumerationException(e);
    }
  }

  public void setCurrentState(T newState) {
    this.currentState = newState;
  }
}

Simpel doch effectief. De code van de excepties en de annotaties zijn zo eenvoudig dat ik hiervan de code achterwege laat. Wat ik wel nog even wil tonen is hoe je dan gebruik maakt van deze class:

public class LightBulb extends Workflow<Switch> {

    public LightBulb() {
        super(Switch.OFF);
    }

    public void switchOff() throws IllegalStateChangeException {
        changeStateTo(Switch.OFF);
    }

    public void switchOn() throws IllegalStateChangeException {
        changeStateTo(Switch.ON);
    }
}

En onderstaande class om het programma te laten draaien:

public class Demo {

   public static void main(String[] args) throws Exception {

        LightBulb lightBulb = new LightBulb();

        System.out.println("CurrentState:" + lightBulb.getCurrentState());

        lightBulb.switchOn();

        System.out.println("CurrentState:" + lightBulb.getCurrentState());

        try {
            lightBulb.switchOn();
        }
        catch (Exception e) {

            System.out.println(e.getMessage());
        }
    }
}

Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development, Java (EE)

Tags: , , , , ,


Reactie

  • Mooi voorbeeld, en erg goed toepasbaar! 90% van de applicaties die ik tegenkom hebben wel iets van een statemachine of workflow nodig.

    Geplaatst op 24 juli 2008 om 11:36 Permalink