web-dev-qa-db-ja.com

ステートマシン設計における循環依存の問題

入力を取り込んで出力を生成することもできる基本的な状態機械の構造を開発しようとしています。ステートと遷移の間の関係をモデル化する方法を理解しようとして、少し精神的な障害にぶつかりました。この設計では、サイクルを持つステートマシンをサポートしていません。

ここに私が持っているものとそれが問題である理由を説明する必要があるいくつかのインターフェースがあります(コンテキストの格納方法とI/Oの処理方法を扱う部分は無視してください。これは単純化です):

public interface State<T extends StateInput> {
    int id();
    ContextAndOutput goContinuing(StorableContext context, Input input);
    ContextAndOutput render(T stateInput, Input input);
}

public interface InitialState<T extends StateInput> extends State<T> {
    ContextAndOutput buildInitial(Input input);
}

public interface Transition<T extends Context> {
    boolean canBeFollowed(T context, Input request);
    ContextAndOutput transition(T context, Input request);
}

public interface Context {
    StorableContext storable();
}

public class ContextAndOutput {
    private final StorableContext context;
    private final Output output;
}

public class StorableContext {
    private final int stateId;
    private final Object data;
}

public class Output {
    private final String raw;
}

public class Input {
    private final String raw;
}

そして、私がそれらが構築されることをどのように想定しているのかを示すためのいくつかの実装例(これは進行中です):

public class Context0 implements Context {

    private final int stateId;
    private final String sampleContextData;

    @Override
    public StorableContext storable() {
        return new StorableContext(stateId, this);
    }
}

public class State0 implements InitialState<State0Input> {

    private final List<Transition<Context0>> transitions;

    @Override
    public ContextAndOutput buildInitial(Input input) {
        System.out.println(stateName() + ": building initial");
        Context0 initial = new Context0(id(), input.getRaw());
        return new ContextAndOutput(initial.storable(), 
                                    new Output("What would you like to buy?"));
    }

    @Override
    public int id() {
        return 0;
    }

    @Override
    public String stateName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public ContextAndOutput goContinuing(StorableContext context, Input input) {
        System.out.println(stateName() + ": go continuing");

        // XXX here is the only place we go StorableContext->Context 
        // in a non type safe way, can we do something else?
        Context0 typedContext = (Context0) context.getData(); 
        System.out.println(String.format(
                "I know that the current user query is '%s' " + 
                "but the original one (from context) is '%s'",
                input.getRaw(),
                typedContext.getInitiateConversationQuery()));
        // XXX do something smarter
        Transition<Context0> bestFitTransition = transitions.iterator().next(); 
        return bestFitTransition.transition(typedContext, input);
    }

    @Override
    public ContextAndOutput render(State0Input stateInput, Input input) {
        System.out.println(stateName() + ": rendering");
        System.out.println(stateName() + ": building initial");
        Context0 initial = new Context0(
                               id(),
                               stateInput.getInitiateConversationQuery());

        return new ContextAndOutput(initial.storable(), new Output("Hello!"));
    }
}

public class OneToZero implements Transition<Context1> {

    private final State0 destination;

    @Override
    public boolean canBeFollowed(Context1 context, Input request) {
        return true;
    }

    @Override
    public ContextAndOutput transition(Context1 context, Input request) {
        // Build StateInput for State1 from context
        State0Input stateInput = null; // TODO
        return destination.render(stateInput, request);
    }
}

public class StateMachine {

    private final InitialState initialState;
    private final Map<Integer, State> everyStateIdToState;

    public StateMachine(
               InitialState initialState, 
               Set<ContinuingState> continuingStates) {

        this.initialState = initialState;
        this.everyStateIdToState = buildStateIdToState(initialState, continuingStates);
    }

    private Map<Integer, State> buildStateIdToState(
                                    InitialState state, 
                                    Set<ContinuingState> continuingStates) {

        Set<State> allStates = new HashSet<>(continuingStates.size() + 1);
        allStates.add(state);
        allStates.addAll(continuingStates);
        return allStates.stream()
                .collect(Collectors.toMap(
                        State::id,
                        Function.identity()));
    }

    public ContextAndOutput initiate(Input input) {
        return initialState.buildInitial(input);
    }

    public ContextAndOutput continuing(StorableContext context, Input input) {
        State currentState = everyStateIdToState.get(context.getStateId());
        return currentState.goContinuing(context, input);
    }
}

したがって、遷移がソース状態と宛先状態に固有である場合は、ソース状態のコンテキストのタイプでパラメーター化して、ソース状態のコンテキストから宛先状態の入力へのタイプの遷移を行えるようにします。

ここに問題があります:このように、遷移は、入力の提供先としてどのオブジェクトを呼び出すかがわかるように、コンストラクターの一部として宛先状態を持っている必要があります。ただし、状態もそのコンストラクターを使用して、次に実行するものを決定できます。重要な場合は、任意の状態から取得できる「グローバル」トランジションを作成して、いくつかの作業を行い、既存の状態に戻すこともできます。状態

この問題は、各方向に1つずつ、2つの状態と2つの遷移がある場合に発生します。

// Can't give this a Transition back to State0
State1 state1 = new State1(Collections.emptyList()); 

List<Transition<Context0>> zerosOutboundTransitions = 
    Collections.singletonList(new ZeroToOne(state1));

//List<Transition<Context0>> onesOutboundTransitions =   
    Collections.singletonList(new OneToZero(state0));

Set<ContinuingState> continuingStates = ImmutableSet.of(state1);

InitialState initialState = new State0(zerosOutboundTransitions);

この循環依存関係を回避または処理するための設計パターンまたは戦略、またはこの設計についてどう考えるべきかについて、何らかの助けを探しています。ここでさまざまな戦略をいくつか試してみましたが、少し途方に暮れています。型の安全性を通じて、柔軟性をほとんど失うことなく、できるだけ多くの構造を適用し、「素晴らしい」ソリューションを見つけるのに苦労しています。ありがとう!

編集:私はこのような循環依存関係を強制できることを理解していますが、この循環依存関係を必要としないようにデザインを変更する方法を理解しようとしています。欠点がいくつかあるためです。

// Define the states
State0 initialState = new State0();
State1 state1 = new State1();

// Define the transitions
List<Transition<Context0>> zerosOutboundTransitions = 
    Collections.singletonList(new ZeroToOne(state1));

List<Transition<Context1>> onesOutboundTransitions = 
    Collections.singletonList(new OneToZero(initialState));

// Inform the states of the transitions they may use
initialState.setTransitions(zerosOutboundTransitions);
state1.setTransitions(onesOutboundTransitions);
4
Jordan

...私の質問は、この循環依存を必要としないようにデザインをどのように変更するかについてです。 –ヨルダン

私が見た1つの方法は、各状態に次の状態を構築させることです。それは機能しますが、ガベージコレクターを悪用しているように感じます。 2つの不変状態からバウンスできるようにするメソッドを次に示します。

interface State {
    boolean process(Context context);
}

enum States implements State {
    A {
        public boolean process(Context context) {
            System.out.println(States.A);
            context.setState(States.B);
            return true;
        }
    }, B {
        public boolean process(Context context) {
            System.out.println(States.B);
            context.setState(States.A);
            return true;
        }
    }
}

class Context{
    State state;

    public Context(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }
}

class Processor {

    public void process(Context context) {
        while(context.getState().process(context));
    }
}

public class EntryPoint {
    public static void main(String[] args) {
        Processor p = new Processor();
        Context cd = new Context(States.A);
        p.process(cd);
    }
}

this に触発されました

3
candied_orange

これは意味がありません:

A a = new A(b);

B b = new B(a);

ここでの根本的な問題は、各オブジェクトがコンストラクターでそれが指しているものを学習する場合、円形オブジェクトグラフを作成できないことです。コンストラクタではなくセッターを許可するものを少なくとも1つ必要とするため、何を指すかがわかる前にセッターが存在できます。

A a = new A();

B b = new B(a);

a.setRef(b);

これで循環参照ができました。

覚えておいてください 循環参照には欠点があります

2
candied_orange