web-dev-qa-db-fra.com

Comment implémenter une machine à états finis FSM dans Java

J'ai quelque chose à faire pour le travail et j'ai besoin de votre aide. Nous voulons implémenter un FSM - Finite State Machine, pour identifier la séquence de caractères (comme: A, B, C, A, C) et dire si elle a été acceptée.

Nous pensons mettre en œuvre trois classes: State, Event et Machine. La classe state présente un nœud dans le FSM, nous avons pensé l'implémenter avec State design pattern, chaque nœud s'étend de l'état de classe abstraite et chaque classe gère différents types d'événements et indique les transitions vers un nouvel état. Est-ce une bonne idée à votre avis?

Deuxième chose, nous ne savons pas comment sauvegarder toutes les transitions. Encore une fois, nous avons pensé l'implémenter avec une sorte de map, qui constitue le point de départ et génère une sorte de vecteur avec les prochains états, mais je ne suis pas sûr que ce soit une bonne idée.

Je serais heureux d’avoir quelques idées sur la façon de le mettre en œuvre ou peut-être pourriez-vous me donner quelques points de départ.

Comment dois-je sauvegarder le FSM, c'est-à-dire comment construire l'arborescence au début du programme? Je l'ai googlé et trouvé beaucoup d'exemples mais rien ne m'aide.

Merci beaucoup.

48
Ofir A.

Le cœur d'une machine à états est la table de transition, qui prend un état et un symbole (ce que vous appelez un événement) à un nouvel état. C'est juste un tableau d'états à deux index. Pour des raisons de sécurité et de type, déclarez les états et les symboles sous forme d'énumérations. J'ajoute toujours un membre "length" (spécifique à la langue) pour vérifier les limites du tableau. Lorsque j'ai codé à la main les FSM, je formate le code en lignes et en colonnes avec un fiddling. Les autres éléments d'une machine à états sont l'état initial et l'ensemble des états acceptants. L'implémentation la plus directe de l'ensemble des états acceptants est un tableau de booléens indexés par les états. En Java, cependant, les énumérations sont des classes et vous pouvez spécifier un argument "accepting" dans la déclaration pour chaque valeur énumérée et l'initialiser dans le constructeur pour l'énumération.

Pour le type de machine, vous pouvez l'écrire en tant que classe générique. Il faudrait deux arguments de type, un pour les états et un pour les symboles, un argument de tableau pour la table de transition et un seul pour l'initiale. Le seul autre détail (même s'il est essentiel) est que vous devez appeler Enum.ordinal () pour obtenir un entier approprié pour l'indexation du tableau de transition, car vous ne disposez pas de syntaxe pour déclarer directement un tableau avec un index d'énumération (bien qu'il faille être).

Pour préempter un problème, EnumMap ne fonctionnera pas pour la table de transition, car la clé requise est une paire de valeurs d'énumération, pas une seule.

enum State {
    Initial( false ),
    Final( true ),
    Error( false );
    static public final Integer length = 1 + Error.ordinal();

    final boolean accepting;

    State( boolean accepting ) {
        this.accepting = accepting;
    }
}

enum Symbol {
    A, B, C;
    static public final Integer length = 1 + C.ordinal();
}

State transition[][] = {
    //  A               B               C
    {
        State.Initial,  State.Final,    State.Error
    }, {
        State.Final,    State.Initial,  State.Error
    }
};
40
eh9

EasyFSM est une bibliothèque dynamique Java qui peut être utilisée pour implémenter un FSM.

Vous pouvez trouver la documentation correspondante à l’adresse suivante: Machine à états finis en Java

Vous pouvez également télécharger la bibliothèque à l’adresse suivante: Bibliothèque Java FSM: DynamicEasyFSM

11
user2968375

Hmm, je suggérerais que vous utilisiez Flyweight pour implémenter les états. Objectif: éviter la surcharge de mémoire d'un grand nombre de petits objets. Les machines d'état peuvent devenir très, très grandes.

http://en.wikipedia.org/wiki/Flyweight_pattern

Je ne suis pas sûr de comprendre le besoin d'utiliser l'état de modèle de conception pour implémenter les nœuds. Les nœuds d'une machine à états sont sans état. Ils font juste correspondre le symbole d'entrée actuel aux transitions disponibles à partir de l'état actuel. C’est-à-dire, à moins d’avoir complètement oublié comment ils fonctionnent (ce qui est une possibilité certaine).

Si je le codais, je ferais quelque chose comme ceci:

interface FsmNode {
  public boolean canConsume(Symbol sym);
  public FsmNode consume(Symbol sym);
  // Other methods here to identify the state we are in
}

  List<Symbol> input = getSymbols();
  FsmNode current = getStartState();
  for (final Symbol sym : input) {
    if (!current.canConsume(sym)) {
      throw new RuntimeException("FSM node " + current + " can't consume symbol " + sym);
    }
    current = current.consume(sym);
  }
  System.out.println("FSM consumed all input, end state is " + current);

Que ferait Flyweight dans ce cas? Eh bien, sous le FsmNode, il y aurait probablement quelque chose comme ça:

Map<Integer, Map<Symbol, Integer>> fsm; // A state is an Integer, the transitions are from symbol to state number
FsmState makeState(int stateNum) {
  return new FsmState() {
    public FsmState consume(final Symbol sym) {
      final Map<Symbol, Integer> transitions = fsm.get(stateNum);
      if (transisions == null) {
        throw new RuntimeException("Illegal state number " + stateNum);
      }
      final Integer nextState = transitions.get(sym);  // May be null if no transition
      return nextState;
    }
    public boolean canConsume(final Symbol sym) {
      return consume(sym) != null;
    }
  }
}

Cela crée les objets d'état en fonction de leurs besoins. Cela vous permet d'utiliser un mécanisme sous-jacent beaucoup plus efficace pour stocker la machine à états réelle. Celui que j'utilise ici (Map (Integer, Map (Symbol, Integer))) n'est pas particulièrement efficace.

Notez que la page Wikipedia se concentre sur les cas où de nombreux objets assez similaires partagent les mêmes données, comme dans l'implémentation String en Java. À mon avis, Flyweight est un peu plus général et couvre toute création à la demande d'objets ayant une courte durée de vie (utilisez plus de CPU pour économiser sur une structure de données sous-jacente plus efficace).

8
Anders Johansen

Considérez la bibliothèque facile et légère Java bibliothèque EasyFlow . À partir de leur documentation:

Avec EasyFlow, vous pouvez:

  • implémenter une logique complexe mais garder votre code simple et propre
  • gérer les appels asynchrones avec facilité et élégance
  • éviter la concurrence en utilisant une approche de programmation événementielle
  • éviter l'erreur StackOverflow en évitant la récursivité
  • simplifier la conception, la programmation et les tests d’applications Java complexes)
7
btiernay

Vous pouvez implémenter une machine à états finis de deux manières différentes.

Option 1:

Machine à états finis avec un flux de travail prédéfini : recommandée si vous connaissez tous les états à l'avance et que la machine à états est presque réparée sans modification à l'avenir.

  1. Identifiez tous les états possibles dans votre application

  2. Identifiez tous les événements dans votre application

  3. Identifiez toutes les conditions de votre application, ce qui peut entraîner une transition d'état

  4. La survenue d'un événement peut provoquer des transitions d'état

  5. Construisez une machine à états finis en décidant un flux de travaux d'états et de transitions.

    par exemple, si un événement 1 se produit à l'état 1, l'état sera mis à jour et l'état de la machine peut toujours être à l'état 1.

    Si un événement 2 se produit dans l’État 1, lors d’une évaluation de certaines conditions, le système passe de l’État 1 à l’État 2.

Cette conception est basée sur les modèles de contexte et .

Jetez un oeil à Finite State Machine classes de prototypes.

Option 2:

Arbres comportementaux: Recommandés en cas de modifications fréquentes du flux de travail de la machine à états. Vous pouvez ajouter dynamiquement un nouveau comportement sans casser l’arbre.

enter image description here

La classe de base Task fournit une interface pour toutes ces tâches, les tâches feuille sont ceux que nous venons de mentionner et les tâches parent sont les nœuds intérieurs qui décident de la tâche à exécuter.

Les tâches ont uniquement la logique dont elles ont besoin pour faire ce qui leur est demandé, toute la logique de décision permettant de savoir si une tâche a démarré ou non, si doit être mis à jour, s'il a fini avec succès, etc. est groupé dans la classe TaskController et ajouté par composition.

Les décorateurs sont des tâches qui "décorent" une autre classe en la recouvrant et en lui donnant une logique supplémentaire.

Enfin, la classe Blackboard est une classe appartenant à l'IA parent à laquelle chaque tâche fait référence. Cela fonctionne comme base de connaissance pour toutes les tâches de feuille

Jetez un coup d'œil à ceci article par Jaime Barrachina Verdia pour plus de détails

6
Ravindra babu

J'ai conçu et mis en œuvre un exemple simple de machine à états finis avec Java.

IFiniteStateMachine : interface publique permettant de gérer la machine à états finis
tels que l'ajout de nouveaux états à la machine à états finis ou le transit vers les états suivants par
actions spécifiques.

interface IFiniteStateMachine {
    void setStartState(IState startState);

    void setEndState(IState endState);

    void addState(IState startState, IState newState, Action action);

    void removeState(String targetStateDesc);

    IState getCurrentState();

    IState getStartState();

    IState getEndState();

    void transit(Action action);
}

IState : interface publique permettant d'obtenir des informations relatives à l'état
tels que le nom de l'état et les mappages aux états connectés.

interface IState {
    // Returns the mapping for which one action will lead to another state
    Map<String, IState> getAdjacentStates();

    String getStateDesc();

    void addTransit(Action action, IState nextState);

    void removeTransit(String targetStateDesc);
}

Action : la classe qui provoquera la transition d'états.

public class Action {
    private String mActionName;

    public Action(String actionName) {
        mActionName = actionName;
    }

    String getActionName() {
        return mActionName;
    }

    @Override
    public String toString() {
        return mActionName;
    }

}

StateImpl : la mise en oeuvre d'IState. J'ai appliqué une structure de données telle que HashMap pour conserver les mappages Action-Etat.

public class StateImpl implements IState {
    private HashMap<String, IState> mMapping = new HashMap<>();
    private String mStateName;

    public StateImpl(String stateName) {
        mStateName = stateName;
    }

    @Override
    public Map<String, IState> getAdjacentStates() {
        return mMapping;
    }

    @Override
    public String getStateDesc() {
        return mStateName;
    }

    @Override
    public void addTransit(Action action, IState state) {
        mMapping.put(action.toString(), state);
    }

    @Override
    public void removeTransit(String targetStateDesc) {
        // get action which directs to target state
        String targetAction = null;
        for (Map.Entry<String, IState> entry : mMapping.entrySet()) {
            IState state = entry.getValue();
            if (state.getStateDesc().equals(targetStateDesc)) {
                targetAction = entry.getKey();
            }
        }
        mMapping.remove(targetAction);
    }

}

FiniteStateMachineImpl : Implémentation de IFiniteStateMachine. J'utilise ArrayList pour conserver tous les états.

public class FiniteStateMachineImpl implements IFiniteStateMachine {
    private IState mStartState;
    private IState mEndState;
    private IState mCurrentState;
    private ArrayList<IState> mAllStates = new ArrayList<>();
    private HashMap<String, ArrayList<IState>> mMapForAllStates = new HashMap<>();

    public FiniteStateMachineImpl(){}
    @Override
    public void setStartState(IState startState) {
        mStartState = startState;
        mCurrentState = startState;
        mAllStates.add(startState);
        // todo: might have some value
        mMapForAllStates.put(startState.getStateDesc(), new ArrayList<IState>());
    }

    @Override
    public void setEndState(IState endState) {
        mEndState = endState;
        mAllStates.add(endState);
        mMapForAllStates.put(endState.getStateDesc(), new ArrayList<IState>());
    }

    @Override
    public void addState(IState startState, IState newState, Action action) {
        // validate startState, newState and action

        // update mapping in finite state machine
        mAllStates.add(newState);
        final String startStateDesc = startState.getStateDesc();
        final String newStateDesc = newState.getStateDesc();
        mMapForAllStates.put(newStateDesc, new ArrayList<IState>());
        ArrayList<IState> adjacentStateList = null;
        if (mMapForAllStates.containsKey(startStateDesc)) {
            adjacentStateList = mMapForAllStates.get(startStateDesc);
            adjacentStateList.add(newState);
        } else {
            mAllStates.add(startState);
            adjacentStateList = new ArrayList<>();
            adjacentStateList.add(newState);
        }
        mMapForAllStates.put(startStateDesc, adjacentStateList);

        // update mapping in startState
        for (IState state : mAllStates) {
            boolean isStartState = state.getStateDesc().equals(startState.getStateDesc());
            if (isStartState) {
                startState.addTransit(action, newState);
            }
        }
    }

    @Override
    public void removeState(String targetStateDesc) {
        // validate state
        if (!mMapForAllStates.containsKey(targetStateDesc)) {
            throw new RuntimeException("Don't have state: " + targetStateDesc);
        } else {
            // remove from mapping
            mMapForAllStates.remove(targetStateDesc);
        }

        // update all state
        IState targetState = null;
        for (IState state : mAllStates) {
            if (state.getStateDesc().equals(targetStateDesc)) {
                targetState = state;
            } else {
                state.removeTransit(targetStateDesc);
            }
        }

        mAllStates.remove(targetState);

    }

    @Override
    public IState getCurrentState() {
        return mCurrentState;
    }

    @Override
    public void transit(Action action) {
        if (mCurrentState == null) {
            throw new RuntimeException("Please setup start state");
        }
        Map<String, IState> localMapping = mCurrentState.getAdjacentStates();
        if (localMapping.containsKey(action.toString())) {
            mCurrentState = localMapping.get(action.toString());
        } else {
            throw new RuntimeException("No action start from current state");
        }
    }

    @Override
    public IState getStartState() {
        return mStartState;
    }

    @Override
    public IState getEndState() {
        return mEndState;
    }
}

exemple :

public class example {

    public static void main(String[] args) {
        System.out.println("Finite state machine!!!");
        IState startState = new StateImpl("start");
        IState endState = new StateImpl("end");
        IFiniteStateMachine fsm = new FiniteStateMachineImpl();
        fsm.setStartState(startState);
        fsm.setEndState(endState);
        IState middle1 = new StateImpl("middle1");
        middle1.addTransit(new Action("path1"), endState);
        fsm.addState(startState, middle1, new Action("path1"));
        System.out.println(fsm.getCurrentState().getStateDesc());
        fsm.transit(new Action(("path1")));
        System.out.println(fsm.getCurrentState().getStateDesc());
        fsm.addState(middle1, endState, new Action("path1-end"));
        fsm.transit(new Action(("path1-end")));
        System.out.println(fsm.getCurrentState().getStateDesc());
        fsm.addState(endState, middle1, new Action("path1-end"));
    }

}

Exemple complet sur Github

1
shanwu