Подтвердить что ты не робот

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

В моем приложении для Android я использую несколько состояний на основе enum. Хотя они работают очень хорошо, то, что я ищу, - это предложение о том, как элегантно принимать события, как правило, из зарегистрированных обратных вызовов или из сообщений 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) каждого состояния, и (b) прохождение дополнительных параметров выглядит неудобно.

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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.

Ответ 4

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

Ответ 5

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

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

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

Ответ 6

Как реализовать обработку событий с помощью посетителей:

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

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

Ответ 7

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

Ответ 8

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