Подход с конечным автоматом для простых переходов между состояниями

Я собираюсь создать очень простой конечный автомат. Мой конечный автомат будет содержать следующие 3 состояния:

public enum States {
    PENDING,
    ACTIVE,
    DONE
}

Здесь возможны несколько переходов + начальные состояния, а именно:

Начальные состояния: PENDING or ACTIVE Переходы:

  1. PENDING -> ACTIVE
  2. PENDING -> DONE
  3. ACTIVE -> DONE

Я ищу подходы к представлению этих состояний и возможный конечный автомат для управления переходами. Я изучил подход на основе перечисления, такой как этот, но я также хочу предоставить клиенту информацию о переходах между состояниями, и я не уверен, разумно ли это при таком подходе.

Я также рассмотрел другие методы, такие как State Pattern, но похоже, что это может быть излишним для такой простой просьбы.

Есть ли у кого-нибудь предложения для простых реализаций конечного автомата, отвечающих этим критериям? Я даже думал о чем-то столь же простом, как использование таблицы переходов для хранения переходов и инкапсуляция в нее концепции состояния, которая будет использовать таблицу переходов для определения следующих возможных состояний.


person thebighoncho    schedule 26.08.2019    source источник
comment
вы можете очень аккуратно использовать projects.spring.io/spring-statemachine, использующий пружинный конечный автомат и проста в использовании   -  person Mehdi Benmesssaoud    schedule 28.08.2019


Ответы (3)


Один из простых вариантов - сохранить переходы в виде I want to transition from X to Y while applying this function. Перечисления хорошо подходят для перечисления всех возможных / допустимых состояний в конечном автомате. Нам нужно что-то, чтобы удерживать наши переходы между состояниями - может быть, Map<StateType, StateType>? Но нам также нужен какой-то State объект и способ его изменить - нам нужен Map<StateType, Map<StateType, Transition>>. Ниже приведен пример кода компиляции, который должен помочь вам начать работу. Вы можете раскрыть объект State, как хотите (может быть, сделать его неизменяемым?) И добавлять переходы на лету.

import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;

class StackOverflowQuestion57661787 {
    enum StateType {
        PENDING,
        ACTIVE,
        DONE
    }

    //Made the name explicit here to ease readability
    public interface Transition extends Function<State, State> { }

    public static class State {
        public StateType type;
        //TODO: some real data to manipulate, or make it immutable
        public Object data;
    }

    public static class StateMachine {
        private final Map<StateType, Map<StateType, Transition>> transitions =
                new EnumMap<>(StateType.class);
        private State state;

        public StateMachine(State initialState) {
            this.state = initialState;
            for (StateType value : StateType.values()) {
                transitions.put(value, new EnumMap<>(StateType.class));
            }
        }

        public void addTransition(
                StateType input,
                StateType output,
                Transition transition
        ) {
            //TODO: handle collisions? multiple transitions for a given 
            // output statetype seems like a strange use-case
            transitions.get(input).put(output, transition);
        }

        public void moveTo(StateType toType) {
            Transition transition = transitions.get(state.type).get(toType);
            if (transition == null) {
                //TODO: handle me
                throw new RuntimeException();
            }
            //transition should modify the states "type" too OR
            //you implement it here
            state = transition.apply(state);
        }

        public State getState() {
            return state;
        }
    }
}

Вам нужно будет найти более сложное / абстрактное решение, если ваш State тип объектов зависит от текущего StateType.

person roookeee    schedule 26.08.2019
comment
Спасибо - это было очень полезно. - person thebighoncho; 26.08.2019

Если вы используете Spring, вы можете рассмотреть Spring Statemachine. https://projects.spring.io/spring-statemachine/

person charlb    schedule 26.08.2019

У меня есть личный дизайн, который я часто использовал, и который я называю «насос». В вашем классе конечного автомата есть функция под названием «насос», которая оценивает состояние и соответственно обновляется. Каждая оценка состояния может потребовать некоторого ввода от внешнего источника (контроллеров), такого как пользователь или ИИ. Эти объекты требуются при инициализации вашего конечного автомата и обычно являются абстрактными реализациями. Затем вы добавляете обратные вызовы событий, которые приложения могут переопределить, чтобы перехватить события. Одним из преимуществ этого подхода является то, что метод «накачки» может выполняться из однопоточной или многопоточной системы.

После того, как ваша машина построена, вы можете легко выполнить модульное тестирование, просто вызвав pump навсегда и предоставив контроллеры, которые возвращают случайные значения. По сути, это был бы «обезьяний» тест, чтобы убедиться, что ваша машина может обрабатывать любую комбинацию входных данных без сбоев.

Затем в вашем приложении вам нужно только указать подходящие контроллеры в зависимости от ситуации.

Ниже показан очень грубый конечный автомат для управления гипотетической игрой в кости. Я опустил большинство деталей, оставив основную часть подхода. Обратите внимание, что одной из реализаций Player.rollDice может быть метод блокировки, который ожидает, пока пользователь нажмет кнопку для продвижения игры. В этой схеме вся логика для управления игрой содержится в машине и может быть протестирована независимо от любого пользовательского интерфейса.

interface Player {
   boolean rollDice();
}

class Game {
   int state;
   Player [] players;
   int currentPlayer;
   int dice;

   void pump() {
      switch (state) {
         case ROLL_DICE:
            if (players[currentPlayer].rollDice()) {
               dice = Math.rand() % 6 + 1;
               onDiceRolled(dice);
               state = TAKE_TURN;
            }
            break;
         case TAKE_TURN:
            ...
            break;
      }
   }

   // base method does nothing. Users can override to handle major state transitions.
   protected void onDiceRolled(int dice) {}
}
person Fracdroid    schedule 27.08.2019