0

I have a class hierarchy with a base State class, several inheriting classes each named after their relevant state, and an FSM class that contains all the states in a particular state machine as well as the logic for moving from one state to another. A simplified version in Python:

class State:
  def __init__(self):
    pass

class InitialState(State):
  def __init__(self):
    super().__init__()
    self.name = "Initial State"

class State1(State):
  def __init__(self):
    super().__init__()
    self.name = "State One"

class State2(State):
  def __init__(self):
    super().__init__()
    self.name = "State Two"

class FinalState(State):
  def __init__(self):
    super().__init__()
    self.name = "Final State"

class FSM:
  def __init__(self):
    self.states = {
      'initial': InitialState(),
      'state1': State1(),
      'state2': State2(),
      'final': FinalState()
    }
    self.current_state = self.states['initial']

  def init_to_state1(self):
    assert self.current_state == self.states['initial']
    self.current_state = self.states['state1']

Notice in my FSM class, I'm instantiating each state and in the init_to_state1 method, I'm moving from an instantiated class to another.

Is this the right thing to do? Or is it better to instantiate each state as the implementing class is moved to it? Should I be destroying states once they've been moved out of? Is this the kinda thing where "it depends" is the right answer? I'm just trying to make sure I'm approaching this design the right way.

Thanks in advance.

2
  • 1
    There's no right or wrong, if it works. There are other considerations like efficiency/performance and readability/maintainability.
    – Erik Eidt
    Jun 12, 2022 at 4:56
  • 1
    This sounds a lot like prematurely asking about optimization, which you generally should not do. For now, do whatever is the most readable, understandable, maintainable. Only diverge from this path, if you actually measure a problem. For your example: no, you will not notice a few milliseconds for extra initialization, and a few bytes for a few objects. This may change, if you have 1000s of states and millions of instances.
    – mtj
    Jun 12, 2022 at 5:42

2 Answers 2

1

There are many ways to model states.

Most programming languages don't model "real world" items directly but rather have their own features that are used to compose solutions that do the modeling we need.

In most imperative programming languages, one of the easiest ways to represent state is by sections of code:

for (;;) {
     // state 1, also start state
     for (;;) {
         // code for state 1: accept input and decide next state
         switch (input()) {
         case 'A' : ... break;      // switch to state 2
         case 'B' : ... continue;   // stay in state 1
         }
         break; // switch states
     }

     // state 2
     for (;;) {
         // code for state 2: accept input and decide next state
         switch (input()) {
         case 'A' : ... break;      // switch to state 1
         case 'B' : ... continue;   // stay in state 2
         }
         break;  // switch states
     }
}

The above is an abstraction of a two state in-fix expression parsing algorithm in use by compilers and debuggers.

Such a two state implementation maintains state without any variables at all — no classes, no enums.  The program knows what state it is in simply by where it is in the code.  Code in the upper section is for state 1 and code in for the lower section is for state 2.


An only slightly more complex state machine might use a common loop for all states, and a have an enum or integer for state:

int currentState = 0;
for (;;) {
    var input = Input ();
    switch (currentState) {
    case 0: // handle the input given being in state 0
        // check input and decide next state
        currentState = 3;
        break;
    case 1: // handle the input given being in state 1
        ...
        break;
    ...
    }
}

The above state machine uses a local variable to maintain the state; each state handles the input and decides the next state.


If your requirements are that the state machine gives up control of the thread between handling of states (or that we have multiple (parallel) instances of the state machine), we can move that local variable in the second example above into a class, and have a method handleState() that is called by the outer program.  Here a single class instance can represent the whole state machine — all the states.


If it is useful to you to have a class for each state that is of course possible, and certainly not incorrect (but potentially overkill).  You could make a virtual method handleState(), and override by each state class.


In summary, I'd suggest first looking at the requirements of the state machine and the context within which the state machine finds itself in the larger program.  Only then would I look at the implementation details for the code of the state machine.  That is to say, contrast that with looking first for a one-size fits all approach to state machines.

1

Using classes just as expensive enums isn't object oriented, it's just plain wrong :-)

A possible object oriented approach would be to implement the state transitions as methods of the state objects, and let the state machine delegate each transition call to the current state. The state performs the necessary actions, and returns the next state. Note that this can nicely cover default transitions in the abstract superclass.

2
  • Interesting. Are you saying that my states attribute in the FSM class is wrong? I guess that makes sense because then I could call current_state = current_state.transition_to_whatever() as the method for transitioning. Thanks!
    – ewhiting
    Jun 12, 2022 at 15:31
  • 1
    It might interest you. State pattern. Note that it's not the state matching who holds the state, but the "stateful" entity. However, you can treat the State machine as a stateful entity as well. Implementing the same pattern.
    – Laiv
    Jun 14, 2022 at 8:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.