Cómo implementar una Máquina de Estados Finitos FSM en Java


Tengo algo que hacer para trabajar y necesito tu ayuda. Queremos implementar un FSM - Finite State Machine, para identificar la secuencia de caracteres(como: A, B, C, A, C), y decir si se acepta.

Pensamos implementar tres clases: State, Event y Machine. La clase state presenta un nodo en el FSM, pensamos implementarlo con State design pattern, cada nodo se extenderá desde el estado de la clase abstracta y cada clase manejará diferentes tipos de eventos e indicará transiciones a un nuevo estado. ¿Es buena idea en su opinión?

Segundo, no sabemos cómo guardar todas las transiciones. Nuevamente pensamos implementarlo con algún tipo de map, que mantenga el punto de partida y obtenga algún tipo de vector con los siguientes estados, pero no estoy seguro de que sea una buena idea.

Estaría encantado de tener algunas ideas de cómo implementarlo o tal vez usted puede darme algunos puntos de partida.

¿Cómo debo guardar el FSM, es decir, cómo debo construir el árbol al principio del programa? Lo busqué en Google y encontré muchos ejemplos pero nada que me ayude.

Muchas Gracias.

Author: cegas, 2012-11-04

6 answers

El corazón de una máquina de estados es la tabla de transición, que toma un estado y un símbolo (lo que está llamando un evento) a un nuevo estado. Eso es sólo una matriz de dos índices de estados. Para seguridad de cordura y tipo, declare los estados y símbolos como enumeraciones. Siempre agrego un miembro "length" de alguna manera (específico del idioma) para verificar los límites de la matriz. Cuando he codificado a mano FSM, formateo el código en formato de fila y columna con fiddling espacios en blanco. Los otros elementos de una máquina de estados son la inicial estado y el conjunto de estados aceptantes. La implementación más directa del conjunto de estados aceptantes es una matriz de booleanos indexados por los estados. En Java, sin embargo, las enumeraciones son clases, y puede especificar un argumento "accepting" en la declaración para cada valor enumerado e inicializarlo en el constructor para la enumeración.

Para el tipo de máquina, puede escribirlo como una clase genérica. Tomaría dos argumentos de tipo, uno para los estados y otro para los símbolos, una matriz argumento para la tabla de transición, un solo estado para la inicial. El único otro detalle (aunque es crítico) es que tienes que llamar a Enum.ordinal () para obtener un entero adecuado para indexar el array de transición, ya que no hay sintaxis para declarar directamente un array con un índice de enumeración (aunque debería haberlo).

Para evitar un problema, EnumMap no funcionará para la tabla de transición, porque la clave requerida es un par de valores de enumeración, no uno solo.

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
    }
};
 36
Author: eh9,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-05 12:27:08

Hmm, yo sugeriría que use Peso Mosca para implementar los estados. Propósito: Evite la sobrecarga de memoria de un gran número de objetos pequeños. Las máquinas estatales pueden ser muy, muy grandes.

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

No estoy seguro de ver la necesidad de usar el Estado de patrón de diseño para implementar los nodos. Los nodos de una máquina de estados son apátridas. Simplemente coinciden con el símbolo de entrada actual con las transiciones disponibles del estado actual. Eso es, a menos que haya olvidado por completo cómo funcionan (lo cual es una posibilidad definitiva).

Si lo estuviera codificando, haría algo como esto:

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);

¿Qué haría el Peso mosca en este caso? Bueno, debajo del FsmNode probablemente habría algo como esto:

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;
    }
  }
}

Esto crea los objetos de Estado sobre una base de necesidad de uso, le permite usar un mecanismo subyacente mucho más eficiente para almacenar la máquina de estado real. El que uso aquí (Map (Integer, Map (Symbol, Integer))) no es particularmente eficiente.

Tenga en cuenta que la página de Wikipedia se centra en los casos en los que muchos objetos algo similares comparten los datos similares, como es el caso de la implementación de cadenas en Java. En mi opinión, Flyweight es un poco más general, y cubre cualquier creación bajo demanda de objetos con una vida útil corta (use más CPU para ahorrar en una estructura de datos subyacente más eficiente).

 8
Author: Anders Johansen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-05 11:52:24

EasyFSM es una biblioteca Java dinámica que se puede utilizar para implementar un FSM.

Puede encontrar documentación para el mismo en : Máquina de Estados finitos en Java

También, puede descargar la biblioteca en : Java FSM Library: DynamicEasyFSM

 8
Author: user2968375,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-08 08:19:04

Considere la biblioteca Java fácil y ligera EasyFlow. De sus documentos:

Con EasyFlow puedes:

  • implemente lógica compleja pero mantenga su código simple y limpio
  • maneja llamadas asíncronas con facilidad y elegancia
  • evite la concurrencia mediante el uso de un enfoque de programación basado en eventos
  • evite el error de StackOverflow evitando la recursión
  • simplifique el diseño, la programación y las pruebas de aplicaciones java complejas
 7
Author: btiernay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-25 17:36:42

Puede implementar la Máquina de Estados Finitos de dos maneras diferentes.

Opción 1:

Máquina de Estados finitos con un flujo de trabajo predefinido: Se recomienda si conoce todos los estados de antemano y la máquina de estados está casi fija sin cambios en el futuro

  1. Identifique todos los posibles estados en su solicitud

  2. Identifique todos los eventos en su aplicación

  3. Identificar todos los condiciones en su solicitud, que pueden conducir a la transición de estado

  4. La ocurrencia de un evento puede causar transiciones de estado

  5. Construya una máquina de estados finitos decidiendo un flujo de trabajo de estados y transiciones.

    Por ejemplo, si se produce un evento 1 en el Estado 1, el estado se actualizará y el estado de la máquina aún puede estar en el estado 1.

    Si ocurre un evento 2 en el Estado 1, en alguna evaluación de condición, el sistema se moverá del Estado 1 a Estado 2

Este diseño se basa en los patrones State y Context.

Eche un vistazo a Clases prototipo de Máquina de Estados Finitos.

Opción 2:

Árboles de comportamiento: Recomendado si hay cambios frecuentes en el flujo de trabajo de la máquina de estado. Puede agregar un nuevo comportamiento dinámicamente sin romper el árbol.

introduzca la descripción de la imagen aquí

La clase base Task proporciona una interfaz para todos estos tareas, las tareas hoja son las que acabamos de mencionar, y las tareas padre son los nodos interiores que deciden qué tarea ejecutar a continuación.

Las Tareas solo tienen la lógica que necesitan para hacer realmente lo que se requiere de ellas, toda la lógica de decisión de si una tarea ha comenzado o no, si necesita actualizarse, si ha terminado con éxito, etc. se agrupa en la clase TaskController, y se añade por composición.

Los decoradores son tareas que "decora" otra clase envolviéndola y dándole lógica adicional.

Finalmente, la clase Blackboard es una clase propiedad de la IA principal a la que cada tarea tiene una referencia. Funciona como una base de datos de conocimiento para todas las tareas de hoja

Echa un vistazo a este artículo de Jaime Barrachina Verdia para más detalles

 6
Author: Ravindra babu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-07 19:15:39

Diseñé e implementé un ejemplo de máquina de estados finitos simple con java.

IFiniteStateMachine : La interfaz pública para administrar la máquina de estados finitos
tales como agregar nuevos estados a la máquina de estados finitos o transitar a los estados siguientes mediante
acciones específicas.

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: La interfaz pública para obtener información relacionada con el estado
como el nombre del estado y las asignaciones a estados conectados.

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 clase que causa la transición de los estados.

public class Action {
    private String mActionName;

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

    String getActionName() {
        return mActionName;
    }

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

}

StateImpl: la implementación de IState. Apliqué una estructura de datos como HashMap para mantener las asignaciones de Estado de acción.

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: Implementación de IFiniteStateMachine. Uso ArrayList para mantener todos los estados.

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;
    }
}

Ejemplo:

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"));
    }

}

Ejemplo Completo en Github

 1
Author: shanwu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-03 17:15:26