我在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)我需要对每个状态的process(event)
中的事件类型进行switch
,而(b)传递额外的参数看起来很尴尬。
我想要一个不使用库的优雅解决方案的建议。
为什么不让事件直接调用正确的状态回调?
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(); }
}
...
}
这用原来的方法解决了您的两个保留:没有"丑陋"的切换,也没有"尴尬"的附加参数。
因此,您希望将事件调度到当前状态的处理程序。
要调度到当前状态,在每个状态变为活动状态时订阅它,在它变为非活动状态时取消订阅它是相当麻烦的。订阅一个知道活动状态的对象更容易,只需将所有事件委派到活动状态即可。
要区分事件,可以使用单独的事件对象,然后使用访问者模式来区分它们,但这相当于样板代码。只有当我有其他代码对所有事件一视同仁时(例如,如果事件必须在交付前缓冲),我才会这样做。否则,我只会做一些类似的事情
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();
}
}
});
}
}
这降低了代码的可读性,但确实消除了为每个事件类型编写委托代码的需要。
如果你的轨道很好,你应该使用与状态机相结合的策略模式。在状态枚举中实现事件处理,提供默认的通用实现,并可能添加特定的实现。
定义您的事件和相关策略界面:
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
的工作。
我不清楚为什么在已经有事件总线的情况下需要回调接口。总线应该能够根据事件类型向侦听器传递事件,而不需要接口。考虑一下像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);
我相信这种方法与你对meritin的回答的第一条评论一致:
与其手动编写类StateMachine来实现相同的接口并将事件委托给currentState,还可以使用反射(或其他东西)来实现自动化。然后,外部类将在运行时注册为这些类的侦听器,并在上委托它们,并在状态进入/退出时注册/注销状态。
您可能想尝试使用命令模式:命令界面对应于类似"something_HAPPNED"的内容。然后,每个枚举值都用一个特定的命令进行实例化,该命令可以通过Reflection进行实例化,并可以运行execute方法(在command接口中定义)。
如果有用的话,还可以考虑国家模式。
如果命令很复杂,也可以考虑Composite模式。
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;
}
};
}
如何使用Visitors:实现事件处理
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());
}
}
在我看来,状态图定义和事件处理代码看起来相当简单。:)而且,只要再做一点工作,就可以使用泛型输入类型。
这是一个老问题,但对于那些通过谷歌搜索偶然发现的人来说,虽然这里没有人提到,但我建议在使用Java枚举实现自己的状态机之前,先检查两个现有的框架。
一个是Spring State Machine,您可以将State和Events建模为枚举,您中的大多数人都熟悉Spring框架,它允许我们使用Spring的几个功能,如依赖注入和Spring可以提供的其他一切。
它非常适合模拟Apparat的生命周期,包括INITIALIZING、STARTED、ERROR、RECOVERING、SHUTTINGDOWN等状态。但我看到很多人都在尝试用它来建模购物图和预订系统,Spring State Machine的内存占用相对较大,可以建模数百万张购物图或预订。
Spring State Machine的另一个缺点是,虽然它有能力在长时间运行的流程中保持自己,但它没有任何机制来适应这些流程中的变化,如果你保持一个流程,并且你必须在10天后恢复它,因为新的软件发布/需求导致业务流程发生了变化,你没有内置的方法来处理它。
我有几个博客,blog1-blog2,展示了如何对Spring State Machine进行编程,如果你想检查的话,特别是模型驱动的方式
但主要是因为我之前提到的缺点,我建议您先看看另一个框架,Akka FSM(有限状态机),它更适合拥有数百万实例的低内存占用,并且能够适应不断变化的长期运行过程。
现在,您可以使用Java使用Akka框架进行开发,但相信我,由于缺少一些语言元素,您不想阅读代码,Scala是一种更适合使用Akka开发的语言,它具有Case Class(看起来与Java Enumeration非常相似)和强大的Switch语句模式匹配功能。
现在我听到你说Scala太复杂了,我无法说服我的项目负责人使用Scala进行开发,为了说服你这一切都是一种选择,我使用Java/Scala混合开发了一个概念验证应用程序,其中所有Scala-Akka有限状态机代码都是从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();
}