Problemi con la dipendenza circolare nella progettazione della macchina di stato

4

Sto cercando di sviluppare la struttura per una macchina di stato di base che possa anche contenere input e produrre output. Ho colpito un po 'un blocco mentale nel tentativo di capire come modellare la relazione tra stati e transizioni, in cui il mio progetto non supporterà la macchina a stati che ha un ciclo.

Ecco un paio di interfacce che dovrebbero descrivere ciò che ho e perché è un problema (ignorare le parti che riguardano il modo in cui il contesto è archiviato e l'I / O è gestito, questa è una semplificazione):

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

E alcune implementazioni di esempio per mostrare come sto immaginando che si costruiscano (nota che questo è in corso):

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

Quindi, se una Transizione è univoca per uno stato di origine e di destinazione, l'ho parametrizzata sul tipo del contesto dello stato di origine, in modo che possa eseguire la transizione del tipo dal contesto dello stato di origine all'input dello stato di destinazione.

Ecco il problema: in questo modo, la transizione deve avere lo stato di destinazione come parte del costruttore in modo tale da sapere a quale oggetto chiamare per fornire input. Tuttavia, lo stato deve anche avere Transition come parte del suo costruttore in modo che possa determinare quale seguire successivamente. Se è importante, mi piacerebbe anche la possibilità di creare Transizioni "globali" che possono essere prese da qualsiasi stato, che funzioneranno e poi torneranno allo stato esistente.

Il problema è dimostrato se provo ad avere due stati e due transizioni, una per ciascuna direzione:

// 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);

Sto cercando aiuto in uno schema di progettazione o in una strategia per evitare o gestire questa dipendenza circolare, o come dovrei pensare a questo progetto. Ho fatto alcuni tentativi su diverse strategie qui e sono un po 'in perdita. Mi piacerebbe rinforzare il più possibile la struttura attraverso la sicurezza del tipo senza perdere molta flessibilità e sto lottando per trovare una soluzione "grande". Grazie!

Modifica: Capisco che POTREI forzare una dipendenza circolare come questa, ma sto cercando di capire come potrei cambiare il progetto per non richiedere questa dipendenza circolare, dato che hanno parecchi svantaggi:

// 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);
    
posta Jordan 18.09.2016 - 03:18
fonte

2 risposte

3

... my question is more around how I should change the design to avoid needing this circular dependency. – Jordan

Un metodo che ho visto è che ogni stato costruisce lo stato successivo. Funziona ma sembra che stia abusando del netturbino. Ecco un metodo che ti consente di rimbalzare avanti e indietro da due stati immutabili.

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

Ispirato questo

    
risposta data 18.09.2016 - 22:55
fonte
2

Questo non ha senso:

A a = new A(b);

B b = new B(a);

Il problema fondamentale qui è che non puoi costruire un grafico ad oggetti circolare se ogni oggetto impara a cosa punta in un costruttore. Ne hai bisogno almeno uno che permetta un setter piuttosto che un costruttore in modo che possa esistere prima di sapere a cosa puntare.

A a = new A();

B b = new B(a);

a.setRef(b);

Ora hai un riferimento circolare.

Tieni presente i riferimenti circolari presentano degli svantaggi .

    
risposta data 18.09.2016 - 04:50
fonte