Search code examples
pythonabstract-classstate-machine

Simplifying State Machines (Python)


I want to know why State Machines use an intermediary "State" class at all (between the Concrete States and the Context). From the model below, this could be done with a simple variable.

For example, here's a simplified "containment" style state machine of sorts, which meets the same goal. What am I losing with this model, compared to using a traditional State Machine model?

Also, side question, these child classes are contained because they're indented and part of the parent (Lift) class. What is the difference to this style of inheritance vs un-indenting them and setting them to something like this: class FloorOne(Lift):

class Lift:
    def __init__(self):
        self.current_state = Lift.FloorOne(self)

    def show_input_error(self):
        print("Incorrect input")

    class FloorOne:
        def __init__(self, lift):
            self.lift = lift
            print("You are on Floor One")

        def button_press(self, num):
            self.transition(num)

        def transition(self, num):
            if num == 2:
                self.lift.current_state = Lift.FloorTwo(self.lift)
            elif num == 3:
                self.lift.current_state = Lift.FloorThree(self.lift)
            else:
                self.lift.show_input_error()

    class FloorTwo:
        def __init__(self, lift):
            self.lift = lift
            print("You are on Floor Two")

        def button_press(self, num):
                self.transition(num)

        def transition(self, num):
            if num == 1:
                self.lift.current_state = Lift.FloorOne(self.lift)
            elif num == 3:
                self.lift.current_state = Lift.FloorThree(self.lift)
            else:
                self.lift.show_input_error()

    class FloorThree:
        def __init__(self, lift):
            self.lift = lift
            print("You are on Floor Three")

        def button_press(self, num):
            self.transition(num)

        def transition(self, num):
            if num == 1:
                self.lift.current_state = Lift.FloorOne(self.lift)
            elif num == 2:
                self.lift.current_state = Lift.FloorTwo(self.lift)
            else:
                self.lift.show_input_error()

lift = Lift()
while True:
    goto = input("What floor would you like to go to?")
    lift.current_state.button_press(int(goto))

Solution

  • If you define all the floor classes as subclasses from a common class State, you gain:

    1. It is clear, from the code, which is the common interface of the concrete states. Even you can enforce an interface adding abstract methods which have to be overriden in the concrete classes in order to be able to be instantiated

    2. You have to code less, because you can define the methods which are equal for all states once, in the State class. For example the button_press method.

    3. Makes code easier to change.

    Look at this code:

    class Lift:
        def __init__(self):
            self.current_state = Lift.FloorOne(self)
    
        def show_input_error(self):
            print("Incorrect input")
            
        class State:
            def __init__(self, lift):
                self.lift = lift
                print(f'You are on Floor {self.floor_name}')
    
            def button_press(self, num):
                self.transition(num)
    
            def transition(self, num):
                if num != self.floor_num and num in [1,2,3]:
                    self.lift.current_state = [Lift.FloorOne, 
                                               Lift.FloorTwo, 
                                               Lift.FloorThree][num - 1](self.lift)
                    
                else:
                    self.lift.show_input_error()
    
        class FloorOne(State):
            floor_name = 'One'
            floor_num = 1
            
        class FloorTwo(State):
            floor_name = 'Two'
            floor_num = 2
    
        class FloorThree(State):
            floor_name = 'Three'
            floor_num = 3
            
    lift = Lift()
    while True:
        goto = input("What floor would you like to go to?")
        lift.current_state.button_press(int(goto))
    

    Now it is easier to add a floor.

    If you want you can override any of the methods in a subclass for a different behavior:

    class BrokenLift(State):
        def transition(self, num):
            print('broken lift!')