web-dev-qa-db-fra.com

Java machine d'état basée sur une énumération (FSM): transmission d'événements

J'utilise plusieurs machines d'état basées sur une énumération dans mon Android. Bien que celles-ci fonctionnent très bien, ce que je recherche est une suggestion sur la façon de recevoir avec élégance des événements, généralement à partir de rappels enregistrés ou des messages Eventbus, à l'état actuellement actif. Parmi les nombreux blogs et didacticiels concernant les FSM basés sur l'énumération, la plupart d'entre eux donnent des exemples de machines d'état qui consomment des données (par exemple, des analyseurs) plutôt que de montrer comment ces FSM peuvent être générés par des événements.

Une machine d'état typique que j'utilise a cette forme:

private State mState;

public enum State {

    SOME_STATE {


        init() {
         ... 
        }


        process() {
         ... 
        }


    },


    ANOTHER_STATE {

        init() {
         ... 
        }

        process() {
         ... 
        }

    }

}

...

Dans ma situation, certains états déclenchent un travail à effectuer sur un objet particulier, enregistrant un auditeur. Cet objet rappelle de manière asynchrone lorsque le travail est terminé. En d'autres termes, juste une simple interface de rappel.

De même, j'ai un EventBus. Les classes qui souhaitent être notifiées des événements implémentent à nouveau une interface de rappel et listen() pour ces types d'événements sur le EventBus.

Le problème de base est donc que la machine à états, ou ses états individuels, ou la classe contenant l'énum FSM, ou quelque chose doit implémenter ces interfaces de rappel, afin qu'elles puissent représenter des événements sur l'état actuel.

Une approche que j'ai utilisée consiste pour l'ensemble du enum à implémenter les interfaces de rappel. L'énumération elle-même a des implémentations par défaut des méthodes de rappel en bas, et les états individuels peuvent ensuite remplacer ces méthodes de rappel pour les événements qui les intéressent. Pour que cela fonctionne, chaque état doit s'inscrire et se désinscrire à son entrée et à sa sortie, sinon il existe un risque de rappel sur un état qui n'est pas l'état actuel. Je vais probablement m'en tenir à cela si je ne trouve rien de mieux.

Une autre façon consiste pour la classe conteneur à implémenter les rappels. Il doit ensuite déléguer ces événements à la machine d'état, en appelant mState.process( event ). Cela signifie que je devrais énumérer les types d'événements. Par exemple:

enum Events {
    SOMETHING_HAPPENED,
    ...
}

...

onSometingHappened() {

    mState.process( SOMETHING_HAPPENED );
}

Je n'aime pas cela cependant car (a) j'aurais la laideur d'avoir besoin de switch sur les types d'événements dans la process(event) de chaque état, et (b) en passant par paramètres semble maladroit.

Je voudrais une suggestion pour une solution élégante pour cela sans avoir recours à une bibliothèque.

34
Trevor

Vous souhaitez donc envoyer des événements à leurs gestionnaires pour l'état actuel.

Pour envoyer à l'état actuel, abonner chaque état lorsqu'il devient actif et le désinscrire lorsqu'il devient inactif est plutôt fastidieux. Il est plus facile de souscrire un objet qui connaît l'état actif et délègue simplement tous les événements à l'état actif.

Pour distinguer les événements, vous pouvez utiliser des objets d'événement distincts, puis les distinguer avec le modèle de visiteur , mais c'est un peu du code passe-partout. Je ne ferais cela que si j'ai un autre code qui traite tous les événements de la même manière (par exemple, si les événements doivent être mis en mémoire tampon avant la livraison). Sinon, je ferais simplement quelque chose comme

interface StateEventListener {
    void onEventX();
    void onEventY(int x, int y);
    void onEventZ(String s);
}

enum State implements StateEventListener {
    initialState {
        @Override public void onEventX() {
            // do whatever
        }
        // same for other events
    },
    // same for other states
}

class StateMachine implements StateEventListener {
    State currentState;

    @Override public void onEventX() {
        currentState.onEventX();
    }

    @Override public void onEventY(int x, int y) {
        currentState.onEventY(x, y);
    }

    @Override public void onEventZ(String s) {
        currentState.onEventZ(s);
    }
}

Modifier

Si vous avez de nombreux types d'événements, il peut être préférable de générer le code de délégation ennuyeux lors de l'exécution à l'aide d'une bibliothèque d'ingénierie de bytecode, ou même d'un proxy JDK simple:

class StateMachine2 {
    State currentState;

    final StateEventListener stateEventPublisher = buildStateEventForwarder(); 

    StateEventListener buildStateEventForwarder() {
        Class<?>[] interfaces = {StateEventListener.class};
        return (StateEventListener) Proxy.newProxyInstance(getClass().getClassLoader(), interfaces, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                try {
                    return method.invoke(currentState, args);
                } catch (InvocationTargetException e) {
                    throw e.getCause();
                }
            }
        });
    }
}

Cela rend le code moins lisible, mais élimine la nécessité d'écrire du code de délégation pour chaque type d'événement.

16
meriton

Pourquoi les événements n'appellent-ils pas directement le bon rappel sur l'état?

public enum State {
   abstract State processFoo();
   abstract State processBar();
   State processBat() { return this; } // A default implementation, so that states that do not use this event do not have to implement it anyway.
   ...
   State1 {
     State processFoo() { return State2; }
     ...
   },
   State2 {
      State processFoo() { return State1; }
      ...
   } 
}

public enum  Event {
   abstract State dispatch(State state);
   Foo {
      State dispatch(State s) { return s.processFoo(); }
   },
   Bar {
      State dispatch(State s) { return s.processBar(); }
   }
   ...
}

Cela répond à vos deux réserves avec l'approche originale: pas de commutateur "laid", et pas de paramètres supplémentaires "maladroits".

23
Dima

Vous êtes sur de bonnes pistes, vous devez utiliser un modèle de stratégie combiné avec votre machine d'état. Implémentez la gestion des événements dans votre énumération d'état, en fournissant une implémentation commune par défaut et ajoutez éventuellement des implémentations spécifiques.

Définissez vos événements et l'interface de stratégie associée:

enum Event
{
    EVENT_X,
    EVENT_Y,
    EVENT_Z;
    // Other events...
}

interface EventStrategy
{
    public void onEventX();
    public void onEventY();
    public void onEventZ();
    // Other events...
}

Ensuite, dans votre State enum:

enum State implements EventStrategy
{
    STATE_A
    {
        @Override
        public void onEventX()
        {
            System.out.println("[STATE_A] Specific implementation for event X");
        }
    },

    STATE_B
    {
        @Override
        public void onEventY()
        {
            System.out.println("[STATE_B] Default implementation for event Y");     
        }

        public void onEventZ()
        {
            System.out.println("[STATE_B] Default implementation for event Z");
        }
    };
    // Other states...      

    public void process(Event e)
    {
        try
        {
            // Google Guava is used here
            Method listener = this.getClass().getMethod("on" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, e.name()));
            listener.invoke(this);
        }
        catch (Exception ex)
        {
            // Missing event handling or something went wrong
            throw new IllegalArgumentException("The event " + e.name() + " is not handled in the state machine", ex);
        }
    }

    // Default implementations

    public void onEventX()
    {
        System.out.println("Default implementation for event X");
    }

    public void onEventY()
    {
        System.out.println("Default implementation for event Y");       
    }

    public void onEventZ()
    {
        System.out.println("Default implementation for event Z");
    }
}

Selon EventStrategy, il existe une implémentation par défaut pour tous les événements. De plus, pour chaque état, une implémentation spécifique, pour un traitement d'événement différent, est possible.

Le StateMachine ressemblerait à ça:

class StateMachine
{
    // Active state
    State mState;

    // All the code about state change

    public void onEvent(Event e)
    {
        mState.process(e);
    }
}

Dans ce scénario, vous faites confiance à mState comme étant l'état actif actuel, tous les événements sont appliqués à cet état uniquement. Si vous voulez ajouter une couche de sécurité, pour désactiver tous les événements pour tous les états non actifs, vous pouvez le faire mais à mon avis, ce n'est pas un bon modèle, ce n'est pas à un State de savoir s'il est actif mais c'est StateMachine job.

6
ToYonos

Je ne comprends pas pourquoi vous avez besoin d'une interface de rappel alors que vous avez déjà un bus d'événements. Le bus doit être en mesure de fournir des événements aux écouteurs en fonction du type d'événement sans avoir besoin d'interfaces. Considérons une architecture comme Guava's (Je sais que vous ne voulez pas recourir à des bibliothèques externes, c'est la conception que je veux porter à votre attention).

enum State {
  S1 {
    @Subscribe void on(EventX ex) { ... }
  },
  S2 {
    @Subscribe void on(EventY ey) { ... }
  }
}

// when a state becomes active
eventBus.register(currentState);
eventBus.unregister(previousState);

Je crois que cette approche va dans le sens de votre premier commentaire à la réponse de Meriton:

Au lieu d'écrire manuellement la classe StateMachine pour implémenter les mêmes interfaces et déléguer des événements à currentState, il pourrait être possible d'automatiser cela en utilisant la réflexion (ou quelque chose). Ensuite, la classe externe s'inscrirait en tant qu'écouteur pour ces classes au moment de l'exécution et les déléguerait, et enregistrerait/annulerait l'enregistrement de l'état lors de son entrée/sortie.

5
ehecatl

Vous voudrez peut-être essayer d'utiliser le modèle de commande : l'interface de commande correspond à quelque chose comme votre "SOMETHING_HAPPENED". Chaque valeur d'énumération est alors instanciée avec une commande particulière, qui peut être instanciée via Reflection et peut exécuter la méthode d'exécution (définie dans l'interface de commande).

Si utile, considérez également le modèle d'état .

Si les commandes sont complexes, considérez également le modèle composite .

4
Manu

Une alternative pour Java 8 pourrait être d'utiliser une interface avec des méthodes par défaut, comme ceci:

public interface IPositionFSM {

    default IPositionFSM processFoo() {
        return this;
    }

    default IPositionFSM processBar() {
        return this;
    }
}

public enum PositionFSM implements IPositionFSM {
    State1 {
        @Override
        public IPositionFSM processFoo() {
            return State2;
        }
    },
    State2 {
        @Override
        public IPositionFSM processBar() {
            return State1;
        }
    };
}
3
vlp

Que diriez-vous d'implémenter la gestion des événements avec les visiteurs:

import Java.util.LinkedHashMap;
import Java.util.LinkedList;
import Java.util.List;
import Java.util.Map;

public class StateMachine {
    interface Visitor {
        void visited(State state);
    }

    enum State {
        // a to A, b to B
        A('a',"A",'b',"B"),
        // b to B, b is an end-state
        B('b',"B") {
            @Override
            public boolean endState() { return true; }
        },
        ;

        private final Map<Character,String> transitions = new LinkedHashMap<>();

        private State(Object...transitions) {
            for(int i=0;i<transitions.length;i+=2)
                this.transitions.put((Character) transitions[i], (String) transitions[i+1]);
        }
        private State transition(char c) {
            if(!transitions.containsKey(c))
                throw new IllegalStateException("no transition from "+this+" for "+c);
            return State.valueOf(transitions.get(c)).visit();
        }
        private State visit() {
            for(Visitor visitor : visitors)
                visitor.visited(this);
            return this;
        }
        public boolean endState() { return false; }
        private final List<Visitor> visitors = new LinkedList<>();
        public final void addVisitor(Visitor visitor) {
            visitors.add(visitor);
        }
        public State process(String input) {
            State state = this;
            for(char c : input.toCharArray())
                state = state.transition(c);
            return state;
        } 
    }

    public static void main(String args[]) {
        String input = "aabbbb";

        Visitor commonVisitor = new Visitor() {
            @Override
            public void visited(State state) {
                System.out.println("visited "+state);
            }
        };

        State.A.addVisitor(commonVisitor);
        State.B.addVisitor(commonVisitor);

        State state = State.A.process(input);

        System.out.println("endState = "+state.endState());
    }
}

La définition du diagramme d'état et le code de gestion des événements semblent plutôt minimes à mon avis. :) Et, avec un peu plus de travail, il peut être fait fonctionner avec un type d'entrée générique.

3
Danny Daglas

Exemple simple si vous n'avez pas d'événements et que vous avez juste besoin du prochain état public enum LeaveRequestState {

    Submitted {
        @Override
        public LeaveRequestState nextState() {
            return Escalated;
        }

        @Override
        public String responsiblePerson() {
            return "Employee";
        }
    },
    Escalated {
        @Override
        public LeaveRequestState nextState() {
            return Approved;
        }

        @Override
        public String responsiblePerson() {
            return "Team Leader";
        }
    },
    Approved {
        @Override
        public LeaveRequestState nextState() {
            return this;
        }

        @Override
        public String responsiblePerson() {
            return "Department Manager";
        }
    };

    public abstract LeaveRequestState nextState(); 
    public abstract String responsiblePerson();
}
0
Pravin