web-dev-qa-db-ja.com

Java列挙ベースの状態マシン(FSM):イベントを渡す

私はAndroidアプリケーションでいくつかの列挙ベースのステートマシンを使用しています。これらは非常にうまく機能しますが、私が探しているのは、通常登録されたコールバックからイベントをエレガントに受信する方法の提案です列挙ベースのFSMに関する多くのブログとチュートリアルの中で、それらのほとんどは、これらのFSMがイベントからどのように駆動されるかを示すのではなく、データ(パーサーなど)を消費する状態マシンの例を提供します。

私が使用している典型的なステートマシンには、次の形式があります。

_private State mState;

public enum State {

    SOME_STATE {


        init() {
         ... 
        }


        process() {
         ... 
        }


    },


    ANOTHER_STATE {

        init() {
         ... 
        }

        process() {
         ... 
        }

    }

}

...
_

私の状況では、いくつかの状態が特定のオブジェクトで行われる作業をトリガーし、リスナーを登録します。そのオブジェクトは、作業が完了すると非同期にコールバックします。つまり、単純なコールバックインターフェイスのみです。

同様に、EventBusがあります。イベントの通知を必要とするクラスは、コールバックインターフェイスとEventBusのこれらのイベントタイプのlisten()を再度実装します。

したがって、基本的な問題は、ステートマシンまたはその個々の状態、または列挙型FSMを含むクラス、またはsomethingがそれらのコールバックインターフェイスを実装する必要があることです。

私が使用した1つのアプローチは、enum全体にコールバックインターフェイスを実装することです。 enum自体の下部にはコールバックメソッドのデフォルトの実装があり、個々の状態は、関心のあるイベントのコールバックメソッドをオーバーライドできます。これが機能するためには、各状態が登録および登録解除する必要があります。現在の状態ではない状態でコールバックが発生するリスクがあります。これ以上良いものが見つからなければ、おそらくこれに固執します。

別の方法は、包含クラスがコールバックを実装することです。次に、mState.process( event )を呼び出して、これらのイベントをステートマシンに委任する必要があります。つまり、イベントタイプを列挙する必要があります。例えば:

_enum Events {
    SOMETHING_HAPPENED,
    ...
}

...

onSometingHappened() {

    mState.process( SOMETHING_HAPPENED );
}
_

ただし、(a)各状態のprocess(event)内のイベントタイプでswitchを使用する必要があり、(b)追加のパラメータは厄介に見えます。

ライブラリを使用することなく、このためのエレガントなソリューションを提案したいと思います。

34
Trevor

そのため、現在の状態のイベントをハンドラーにディスパッチします。

現在の状態にディスパッチするには、アクティブになると各状態をサブスクライブし、非アクティブになるとサブスクライブを解除するのはかなり面倒です。アクティブ状態を認識し、すべてのイベントをアクティブ状態に委任するオブジェクトをサブスクライブする方が簡単です。

イベントを区別するには、個別のイベントオブジェクトを使用し、 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();
                }
            }
        });
    }
}

これにより、コードは読みにくくなりますが、イベントタイプごとに委任コードを記述する必要がなくなります。

16
meriton

イベントが状態に対して適切なコールバックを直接呼び出さないのはなぜですか?

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

これは、「ugい」スイッチや「厄介な」追加パラメータがないという、元のアプローチで両方の予約に対処します。

23
Dima

順調に進んでいます。ステートマシンと組み合わせて 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で:

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ジョブです。

6
ToYonos

既にイベントバスを持っているのに、なぜコールバックインターフェイスが必要なのかは明確ではありません。バスは、インターフェイスを必要とせずに、イベントタイプに基づいてリスナーにイベントを配信できる必要があります。 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に委任する代わりに、リフレクション(または何か)を使用してこれを自動化することができます。次に、実行時に外部クラスがこれらのクラスのリスナーとして登録してそれらを委任し、状態の開始/終了時に状態を登録/登録解除します。

5
ehecatl

コマンドパターン を使用してみてください。コマンドインターフェイスは、「SOMETHING_HAPPENED」のようなものに対応します。各列挙値は、特定のコマンドでインスタンス化されます。これは、Reflectionを介してインスタンス化でき、executeメソッド(Commandインターフェイスで定義)を実行できます。

役立つ場合は、 状態パターン も考慮してください。

コマンドが複雑な場合は、 複合パターン も考慮してください。

4
Manu

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

訪問者とのイベント処理の実装はどうですか:

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

私の意見では、状態図の定義とイベント処理コードはかなり最小限に見えます。 :)そして、もう少し作業を行うと、汎用入力タイプで動作するようにできます。

3
Danny Daglas

イベントがなく、次のステータスが必要な場合の単純な例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();
}
0
Pravin