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

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

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

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) каждого состояния, и (б) передача дополнительных параметров выглядит неудобно.

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


person Trevor    schedule 30.08.2014    source источник
comment
В чем элегантность? Какой аспект предыдущих решений вы бы хотели улучшить?   -  person meriton    schedule 05.01.2015
comment
Привет, меритон! Хорошая точка зрения; Я должен поправить вопрос, чтобы прояснить. Я предвидел ситуацию, когда сами enums могли бы реализовать интерфейсы слушателя таким образом, чтобы избежать необходимости прослушивать и отправлять события из внешнего контекста в текущее состояние enum. Другими словами, есть ли способ избежать эффективного определения и прослушивания событий дважды. Я понимаю, что это нереалистичный или невозможный вопрос. Хотя были выдвинуты некоторые умные идеи (например, dudeprgm), я склоняюсь к тому, чтобы придерживаться текущего кода, основанного на вашем ответе.   -  person Trevor    schedule 05.01.2015


Ответы (8)


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

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

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

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

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

person meriton    schedule 30.08.2014
comment
Спасибо - очень признателен. Вы заставили меня задуматься - мне интересно, можно ли вместо того, чтобы вручную писать class StateMachine для реализации тех же interfaces и делегировать события currentState, можно автоматизировать это с помощью отражения (или чего-то еще). Пример: каждое состояние enum может иметь метод registerForEventTypes, который возвращает интересующего его слушателя classes. Затем внешний класс будет регистрироваться в качестве слушателя для этих классов во время выполнения и делегировать их, а также регистрировать / отменять регистрацию состояния при входе / выходе. (Не обращайте на меня внимания, просто думайте здесь вслух.) - person Trevor; 30.08.2014
comment
Вероятно, было бы проще всегда доставлять события и обеспечивать реализацию методов слушателя по умолчанию, которая ничего не делает. И да, вы можете использовать генерацию байтового кода для автоматизации написания методов делегирования, например, с java.lang.reflect.Proxy, или с полноценной библиотекой разработки байтового кода, такой как Javassist. Однако для менее чем дюжины типов событий, вероятно, проще и понятнее написать код вручную. - person meriton; 30.08.2014
comment
Разве использование прокси и вызов методов с использованием отражения не будет медленным, особенно на мобильном устройстве Android? - person ; 06.01.2015
comment
Это зависит от того, как часто происходят такие переходы между состояниями. Я ожидал, что с помощью этого подхода вы сможете пересылать миллионы переходов состояний в секунду. - person meriton; 06.01.2015
comment
Незначительные моменты: в вашем StateMachine вы захотите 1) реализовать StateEventListener и 2) сделать методы общедоступными как в State, так и в StateMachine - person vphilipnyc; 29.03.2016

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

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

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

person Dima    schedule 05.01.2015
comment
Это то же самое, что и ответ @meriton. Все состояния должны реализовывать все события, даже если они не используют их. Это то, чего ОП пыталась избежать. - person ; 05.01.2015
comment
Не совсем. Вы можете предоставить для этого реализацию по умолчанию. Я отредактирую свой ответ, чтобы отразить это. - person Dima; 05.01.2015

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

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

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 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 работа.

person ToYonos    schedule 05.01.2015

Мне непонятно, зачем вам интерфейс обратного вызова, если у вас уже есть шина событий. Шина должна иметь возможность доставлять события слушателям в зависимости от типа события без необходимости в интерфейсах. Рассмотрим такую ​​архитектуру, как 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, можно было бы автоматизировать это с помощью отражения (или чего-то еще). Затем внешний класс будет регистрироваться в качестве слушателя для этих классов во время выполнения и делегировать их, а также регистрировать / отменять регистрацию состояния при входе / выходе.

person ehecatl    schedule 09.01.2015

Вы можете попробовать использовать шаблон команды: интерфейс команды соответствует чему-то вроде вашего "ЧТО-ТО_СЛУЧИЛОСЬ" . Таким образом, каждое значение перечисления создается с помощью конкретной команды, которая может быть создана с помощью Reflection и может запускать метод выполнения (определенный в командном интерфейсе).

Если полезно, рассмотрите также шаблон состояния.

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

person Manu    schedule 30.08.2014
comment
Спасибо, Ману. Сейчас я рассмотрю эти шаблоны и предложения. - person Trevor; 30.08.2014

Альтернативой для 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;
        }
    };
}
person vlp    schedule 17.10.2015

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

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

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

person Danny Daglas    schedule 09.01.2015

Простой пример, если у вас нет событий, и вам просто нужен следующий общедоступный список со статусом 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();
}
person Pravin Bansal    schedule 04.06.2019