基于Java枚举的状态机(FSM):传递事件

时间:2014-08-30 09:58:34

标签: java enums fsm state-machine

我在Android应用程序中使用了几个基于枚举的状态机。虽然这些工作非常好,但我正在寻找的是如何优雅地接收事件,通常是从已注册的回调或从事件总线消息接收到当前活动状态的建议。在有关基于枚举的FSM的许多博客和教程中,大多数都提供了使用数据(例如解析器)的状态机的示例,而不是展示如何从事件中驱动这些FSM。

我正在使用的典型状态机具有以下形式:

private State mState;

public enum State {

    SOME_STATE {


        init() {
         ... 
        }


        process() {
         ... 
        }


    },


    ANOTHER_STATE {

        init() {
         ... 
        }

        process() {
         ... 
        }

    }

}

...

在我的情况下,一些状态会触发一项特定对象的工作,注册一个监听器。该工作完成后,该对象将异步回调。换句话说,只是一个简单的回调接口。

同样,我有一个EventBus。希望再次通知事件的类为EventBus上的事件类型实现回调接口和listen()

因此,基本问题是状态机或其各个状态,或包含枚举FSM的类或某些必须实现这些回调接口,以便它们可以代表现状。

我使用的一种方法是让整个enum实现回调接口。枚举本身在底部具有回调方法的默认实现,然后各个状态可以覆盖他们感兴趣的事件的回调方法。为此,每个状态必须在进入和退出时注册和取消注册,否则在不是当前状态的状态下发生回调的风险。如果我找不到更好的东西,我可能会坚持这一点。

另一种方法是包含类来实现回调。然后,它必须通过调用mState.process( event )将这些事件委托给状态机。这意味着我需要枚举事件类型。例如:

enum Events {
    SOMETHING_HAPPENED,
    ...
}

...

onSometingHappened() {

    mState.process( SOMETHING_HAPPENED );
}

我不喜欢这样,因为(a)我对每个州的switch内的事件类型需要process(event)的丑陋,以及(b)通过额外的参数看起来很尴尬。

我想建议一个优雅的解决方案,而不需要使用库。

8 个答案:

答案 0 :(得分:23)

为什么没有事件直接在状态上调用正确的回调?

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

这解决了原始方法的两种保留:没有“丑陋”的开关,也没有“尴尬”的附加参数。

答案 1 :(得分:16)

因此,您希望将事件分派给其处理程序以获取当前状态。

要调度到当前状态,在每个状态变为活动状态时对其进行预订,并在其变为非活动状态时取消订阅是相当麻烦的。订阅一个知道活动状态的对象会更容易,只需将所有事件委托给活动状态。

要区分事件,您可以使用单独的事件对象,然后使用visitor pattern区分它们,但这是相当多的样板代码。我只会这样做,如果我有其他代码将所有事件都视为相同(例如,如果事件必须在交付之前缓冲)。否则,我只是做一些像

这样的事情
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 :(得分:6)

您处于良好的轨道上,您应该使用Strategy pattern与状态机结合使用。在您的状态枚举中实现事件处理,提供默认的通用实现,并可能添加特定的实现。

定义您的活动和相关的策略界面:

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工作。

答案 3 :(得分:5)

我不清楚为什么你已经拥有一个事件总线时需要一个回调接口。总线应该能够根据事件类型向侦听器传递事件,而无需接口。考虑像Guava's这样的架构(我知道你不想求助于外部库,这是我想引起你注意的设计)。

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,而是可以使用反射(或其他东西)自动化它。然后外部类将在运行时注册为这些类的侦听器并委托它们,并在进入/退出时注册/取消注册状态。

答案 4 :(得分:3)

您可能想尝试使用Command pattern:命令界面对应于您的&#34; SOMETHING_HAPPENED&#34;。然后,每个枚举值都使用特定命令进行实例化,该命令可以通过Reflection实例化,并且可以运行execute方法(在Command接口中定义)。

如果有用,请同时考虑State pattern

如果命令很复杂,请同时考虑Composite pattern

答案 5 :(得分:3)

如何使用访问者实现事件处理:

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

在我看来,状态图定义和事件处理代码看起来很小。 :)而且,通过更多的工作,它可以使用通用输入类型。

答案 6 :(得分:3)

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

答案 7 :(得分:0)

简单示例,如果您没有事件,仅需要下一个状态     公共枚举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();
}