One way to look at this is that a finite state machine is basically just a function + a state variable, that, when given an input, updates the state variable, and produces an (input & state)-dependent output. And maybe has some side-effects on state transitions.
This means that, instead of having a bunch of tangled if-statements, you can have your FSMs interact by feeding the output of one FSM into another. Even if you have a FSM that currently has no outputs and simply invokes a transition on a different FSM, change it so that it returns an output representing its current state, and let the other FSM make a decision using that as input. Sometimes, if two or more states have the same effect on some other FSM, and no other FSM cares about the difference, you can encode those states as the same output value. Note that you'll likely have to invent outputs other than "xEvent" and "yEvent" for the later part of the flow. Think about what the FSM actually computes (what it actually does), and design your outputs to reflect that (and serve as meaningful inputs later on).
What this does is, it frees main() from having to examine each event and state machine in order to decide which method to call. Instead, it can focus on orchestrating the flow of inputs and outputs, and let the FSM themselves decide how to handle events internally.
Keep the core logic of each FSM in one place, and keep it self contained. E.g.
// this is basically a constructor
function createFsm(/* parameters, if any */) {
var state = "initial_state";
function transitionFromInitialState(input) {
// switch on input and update 'state' var
// invoke any side effects (DO NOT transition other FSMs here)
// e.g., you can do 'counter++' here
// return output
}
function transitionFromState1(input) {
// switch on input and update 'state' var
// invoke any side effects (DO NOT transition other FSMs here)
// return output
}
function transitionFromState2(input) {
// switch on input and update 'state' var
// invoke any side effects (DO NOT transition other FSMs here)
// return output
}
// ...
return {
transition: function (input) {
switch(state) {
case state_initial: return transitionFromInitialState(input);
case state_1: return transitionFromState1(input);
case state_2: return transitionFromState2(input);
...
}
}
};
}
This is not that dissimilar from the solution you came up with - except that I pulled all the related code together, and introduced outputs.
If all your state machines share the same logic, but start out in different states, consume different (sub)sets of inputs, or something along those lines, then you need just this single creator function - that perhaps takes some parameters to choose an initial state. You then just create 7 instances.
// in main
let fsm1 = createFsm("initial_state1");
let fsm2 = createFsm("initial_state2");
let fsm3 = createFsm("initial_state3");
...
If your state machines have different logic, then provide a creator function for each distinct behavior.
// in main
let fsm1 = createFsmA();
let fsm2 = createFsmB();
let fsm3 = createFsmB();
let fsm4 = createFsmC();
...
Write the orchestration logic:
// Maybe this is called in a loop, or invoked as a chain of
// computations every time a specific initiating event happens
// (meaning, some of the final outputs may be returned, and can
// become part of the input during the next iteration/event)
function compute(initialInput) {
var value = initialInput;
// Direct output ---to--> input chain
value = fsm1.transition(value);
value = fsm2.transition(value);
value = fsm3.transition(value);
// Same output as input to two FSMs
var valueFsm4 = fsm4.transition(value);
var valueFsm5 = fsm5.transition(value);
// Result of two FSMs as input to the next one
value = fsm5.transition(valueFsm4, valueFsm5);
// etc... Think of it as of a flowchart
return /* whatever return values are relevant */
}
Finally, in main:
// maybe there's a loop
var lastComputationResult = null;
while (running()) {
var initialInput = getInitialFsmInput(lastComputationResult);
lastComputationResult = compute(initialInput);
}
------------------------------------------------------------
// maybe it's done in an event handler
var lastComputationResult = null;
addEventHandler(function(event) {
var initialInput = getInitialFsmInput(event, lastComputationResult);
lastComputationResult = compute(initialInput);
});
------------------------------------------------------------
// or some other variation
// ...
Adjust the transition functions (and inputs and outputs) as necessary for what you're doing. E.g., an "input" can be a combination of values (so your transition functions can take either a composite object, or several parameters) - but keep things manageable (you don't want a combinatorial explosion). Parts of the inputs or outputs could also be unrelated to the transition logic itself, but can just serve as the parameters to the side-effects.
fsm1Check
seems to directly invoke a transition on a different FSM, instead of invoking FSM2's client-facing interface? Or is the client-facing interface represented by the events ("xEvent", "yEvent")? Or is FSM2 actually a different state in the same FSM? You showed us bits and pieces, but it's hard to complete the puzzle. Are you using other global variables (x
,y
) as event parameters? What is thecounter
for and why it's the responsibility of the FSMs to increment it? Do they use it at all? 2/2