Конечный автомат на основе перечислений Java (FSM): передача событий

Я использую несколько конечных автоматов на основе enum в своем приложении для Android. Хотя они работают очень хорошо, я ищу предложение о том, как элегантно получать события, обычно из зарегистрированных обратных вызовов или сообщений eventbus, в текущее активное состояние. Из многих блогов и учебных пособий, касающихся FSM на основе перечисления, большинство из них приводят примеры конечных автоматов, которые потребляют данные (например, анализаторы), а не показывают, как эти FSM могут управляться событиями.

Типичный конечный автомат, который я использую, имеет такую ​​форму:

private State mState;

public enum State {

    SOME_STATE {


        init() {
         ... 
        }


        process() {
         ... 
        }


    },


    ANOTHER_STATE {

        init() {
         ... 
        }

        process() {
         ... 
        }

    }

}

...

В моей ситуации некоторые состояния запускают часть работы над конкретным объектом, регистрируя слушателя. Этот объект асинхронно перезванивает, когда работа завершена. Другими словами, просто интерфейс обратного вызова.

Точно так же у меня есть EventBus. Классы, желающие получать уведомления о событиях, снова реализуют интерфейс обратного вызова и listen() для тех типов событий на EventBus.

Следовательно, основная проблема заключается в том, что конечный автомат, или его отдельные состояния, или класс, содержащий перечисление FSM, или что-то еще должны реализовывать эти интерфейсы обратного вызова, чтобы они могли представлять события в текущем состоянии.

Один подход, который я использовал для всего enum реализовать интерфейс (ы) обратного вызова. У самого перечисления внизу есть реализации по умолчанию методов обратного вызова, и отдельные состояния могут затем переопределить эти методы обратного вызова для событий, в которых они заинтересованы. Чтобы это работало, каждое состояние должно регистрироваться и отменяться при входе и выходе, в противном случае существует риск обратного вызова в состоянии, которое не является текущим состоянием. Я, вероятно, буду придерживаться этого, если не найду ничего лучшего.

Другой способ - для содержащего класса реализовать обратные вызовы. Затем он должен делегировать эти события на конечный автомат, вызывая mState.process( event ), Это означает, что мне нужно будет перечислять типы событий. Например:

enum Events {
    SOMETHING_HAPPENED,
    ...
}

...

onSometingHappened() {

    mState.process( SOMETHING_HAPPENED );
}

Однако мне это не нравится, потому что (а) у меня было бы уродство необходимости switch на типах событий в пределах process(event) каждого состояния, и (б) прохождение через дополнительные параметры выглядит неловко.

Я хотел бы предложить элегантное решение для этого, не прибегая к использованию библиотеки.

9 ответов

Решение

Таким образом, вы хотите отправлять события их обработчикам для текущего состояния.

Для отправки в текущее состояние подписка на каждое состояние, когда оно становится активным, и отмена подписки, когда оно становится неактивным, довольно громоздко. Проще подписать объект, который знает активное состояние, и просто делегирует все события в активное состояние.

Чтобы различать события, вы можете использовать отдельные объекты событий, а затем различать их по шаблону посетителя, но это довольно много стандартного кода. Я сделал бы это, только если у меня есть другой код, который обрабатывает все события одинаково (например, если события должны быть помещены в буфер перед доставкой). В противном случае, я бы просто сделал что-то вроде

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

редактировать

Если у вас много типов событий, может быть лучше сгенерировать скучный код делегирования во время выполнения, используя библиотеку инженерных данных байт-кода или даже простой прокси-сервер JDK:

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

Это делает код менее читаемым, но устраняет необходимость в написании кода делегирования для каждого типа события.

Почему бы не сделать так, чтобы события вызывали правильный обратный вызов состояния напрямую?

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(); }
   }
   ...
}

Это относится к обеим вашим оговоркам с оригинальным подходом: без "уродливого" переключателя и без "неловких" дополнительных параметров.

Вы на хороших путях, вы должны использовать шаблон Стратегии в сочетании с вашим конечным автоматом. Реализуйте обработку событий в вашем перечислении состояний, предоставляя стандартную реализацию по умолчанию и, возможно, добавьте конкретные реализации.

Определите ваши события и связанный интерфейс стратегии:

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

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

Тогда в вашем 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");
    }
}

В соответствии с EventStrategyесть реализация по умолчанию для всех событий. Более того, для каждого состояния возможна конкретная реализация для другой обработки событий.

StateMachine будет выглядеть так:

class StateMachine
{
    // Active state
    State mState;

    // All the code about state change

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

В этом случае вы доверяете mState как текущему активному состоянию, все события применяются только к этому состоянию. Если вы хотите добавить слой безопасности, чтобы отключить все события для всех неактивных состояний, вы можете сделать это, но, на мой взгляд, это не очень хорошая модель, это не до State знать, если он активен, но это StateMachine работа.

Мне не понятно, зачем вам нужен интерфейс обратного вызова, когда у вас уже есть шина событий. Шина должна быть способна доставлять события слушателям на основе типа события без необходимости в интерфейсах. Рассмотрим архитектуру, подобную архитектуре Guava (я знаю, что вы не хотите прибегать к внешним библиотекам, я хочу обратить ваше внимание на дизайн).

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

Я полагаю, что этот подход соответствует линии вашего первого комментария к ответу Меритона:

Вместо того, чтобы вручную писать класс StateMachine для реализации тех же интерфейсов и делегирования событий в currentState, можно было бы автоматизировать это с помощью отражения (или чего-то еще). Тогда внешний класс будет регистрироваться как прослушиватель для этих классов во время выполнения и делегировать их, а также регистрировать / отменять регистрацию состояния при его входе / выходе.

Вы можете попробовать использовать шаблон Command: интерфейс Command соответствует чему-то вроде вашего "SOMETHING_HAPPENED". Каждое значение перечисления затем создается с помощью конкретной команды, которая может быть создана с помощью Reflection и может запустить метод execute (определенный в интерфейсе Command).

Если это полезно, рассмотрите также модель состояния.

Если команды сложные, рассмотрите также составной шаблон.

Альтернативой для Java 8 может быть использование интерфейса с методами по умолчанию, например:

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

Как насчет реализации обработки событий с посетителями:

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

На мой взгляд, определение диаграммы состояний и код обработки событий выглядят минимально.:) И, немного потрудившись, его можно заставить работать с универсальным типом ввода.

Что ж, это старый вопрос, но для людей, которые наткнулись на него через поиск Google, хотя здесь никто не упомянул, я советую проверить две существующие структуры, прежде чем внедрять свои собственные конечные автоматы с перечислениями Java.

Одним из них является Spring State Machine , с помощью которого вы можете моделировать свое состояние и события в виде перечислений. Большинство из вас знакомы с инфраструктурой Spring, которая позволяет нам использовать несколько функций Spring, таких как внедрение зависимостей и все остальное, что может предложить Spring.

Он отлично подходит для моделирования жизненного цикла аппарата с такими состояниями, как ИНИЦИАЛИЗАЦИЯ, ЗАПУСК, ОШИБКА, ВОССТАНОВЛЕНИЕ, ВЫКЛЮЧЕНИЕ и т. д., но я вижу, что многие люди пытаются моделировать с его помощью Карту покупок, Систему резервирования, память. След Spring State Machine относительно велик, чтобы моделировать миллионы торговых диаграмм или резервирований.

Еще один недостаток Spring State Machine, хотя он может сохранять себя для длительных процессов, но у него нет никакого механизма для адаптации к изменениям в этих процессах, если вы сохраняете процесс и вам нужно его восстановить, скажем, 10 дней. позже, когда в бизнес-процессе произошли изменения из-за новой версии / требований программного обеспечения, у вас нет встроенных средств для решения этой проблемы.

У меня есть несколько блогов, blog1 blog2 , демонстрирующих, как вы можете программировать Spring State Machine, особенно на основе модели, если вы хотите это проверить.

Но в основном из-за недостатков, о которых я упоминал ранее, я советую вам сначала посмотреть на другой фреймворк, Akka FSM (Конечный автомат), который больше подходит с его небольшим объемом памяти, чтобы иметь миллионы и миллионы экземпляров и имеет возможность адаптироваться к изменяющимся длительным работам. процессы.

Теперь вы можете разрабатывать с помощью Akka framework с Java, но, поверьте мне, из-за некоторых отсутствующих языковых элементов вы не хотите читать код, Scala является гораздо более подходящим языком для разработки с Akka с его классами Case (которые понравятся многим аналогично Java Enumeration) и мощные возможности сопоставления с образцом с операторами Switch.

Теперь я слышу, как вы говорите, что Scala слишком сложна, я не могу убедить мой проект вести разработку с помощью Scala, чтобы убедить вас, что все это вариант, я разработал приложение Proof of Concept, используя гибрид Java/Scala со всеми Scala Akka Finite Код конечного автомата, созданный из модели UML, если вы хотите проверить его здесь, ссылки на блоги, blog3 blog4.

Я надеюсь, что эта информация поможет вам.

Простой пример, если у вас нет событий и вам просто нужен следующий общедоступный статус: перечисление 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();
}
Другие вопросы по тегам