Skip to content

State

There is usually some configuration in complex applications. This configuration affects the reference values in the objects, and the specific field values of such objects can affect the behavior of the application during the execution of a certain process. The above description briefly represents the behavior of the State pattern. It describes how an object changes its behavior as a response to a certain (same) request, depending on its state.

Design and example

Let's assume that we are writing software for an automatic parking vending machine where we can pay for parking by credit card. The cash register has a display with a message, a card sensor and a button for printing a ticket. Depending on the current state of such a cash register (e.g. no paper to print the ticket, no payment for parking or after paying the fee) pressing the print button may be successful (print the ticket) or not.

Without using the State pattern, the state would be managed inside the class representing the parking cash register:

import enum
import datetime


class MoneyMachineState(enum.Enum):
    NO_PAPER = 1
    NEED_PAYMENT = 2
    PAID_READY_TO_PRINT = 3
    UNAVAILABLE = 4
class ParkingTicketVendingMachine:
    def __init__(self):
        self._state = MoneyMachineState.NEED_PAYMENT
        self._printing_paper_pieces = 100
        self._message = ''

    def add_printing_paper_pieces(self, pieces):
        self._printing_paper_pieces += pieces
        if self._state == MoneyMachineState.NO_PAPER:
            self._state = MoneyMachineState.NEED_PAYMENT
        self._message = "Please pay for the parking"

    def pay_for_one_hour_with_credit_card(self):
        if self._state == MoneyMachineState.NEED_PAYMENT:
            print("Paying $5 for parking")
            self._state = MoneyMachineState.PAID_READY_TO_PRINT
        self._message = "Please click the button to print the ticket"

    def print_ticket(self):
        if self._state == MoneyMachineState.PAID_READY_TO_PRINT:
            self._printing_paper_pieces -= 1
            print(f"Ticket valid thru {datetime.datetime.now() + datetime.timedelta(hours=1)}")
            if self._printing_paper_pieces == 0:
                self._state = MoneyMachineState.NO_PAPER
            else:
                self._state = MoneyMachineState.NEED_PAYMENT
        self._message = "Ticket printed. Please collect it."

    def go_down(self):
        if self._state == MoneyMachineState.PAID_READY_TO_PRINT:
            print("Trying to revert last transaction")
        self._state = MoneyMachineState.UNAVAILABLE
        self._message = "Money machine unavailable"

The ParkingTicketVendingMachine class, despite a very simplified implementation and only four states, already has a lot of logic. We can transfer this logic to classes that trigger logic at the vending machine based on the state, but also manage its state.

We make the following changes:

  • we create the ParkingTicketVendingMachineState interface that allows you to perform activities at the ticket office based on the state in which we are
  • we are adding four implementations of this interface - they manage the cash and set the status accordingly
  • we are removing the state switching logic from the ParkingTicketVendingMachine class

Using the State pattern, the implementation could look like this:

import enum
import datetime


class MoneyMachineState(enum.Enum):
    NO_PAPER = 1
    NEED_PAYMENT = 2
    PAID_READY_TO_PRINT = 3
    UNAVAILABLE = 4
class ParkingTicketVendingMachine:
    def __init__(self):
        self._state = MoneyMachineState.NEED_PAYMENT
        self._printing_paper_pieces = 100
        self._message = ''

    def set_message(self, message):
        self._message = message
        print(f"MESSAGE: {message}")

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        self._state = value

    def get_printing_paper_pieces(self):
        return self._printing_paper_pieces

    def add_printing_paper_pieces(self, pieces):
        self._printing_paper_pieces += pieces
        self._message = "Please pay for the parking"

    def pay_for_one_hour_with_credit_card(self):
        print("Paying $5 for parking")
        self._message = "Please click the button to print the ticket"

    def print_ticket(self):
        self._printing_paper_pieces -= 1
        print(f"Ticket valid thru {datetime.datetime.now() + datetime.timedelta(hours=1)}")
        self._message = "Ticket printed. Please collect it."

    def go_down(self):
        print("Trying to revert last transaction")
        self._message = "Vending machine unavailable"
class ParkingTicketVendingMachineState:
    def move_credit_card_to_sensor(self):
        pass

    def press_printing_button(self):
        pass

    def open_machine_and_add_printing_paper_pieces(self):
        pass
class NoPrintingPaperState(ParkingTicketVendingMachineState):
    def __init__(self, machine):
        self._machine = machine

    def move_credit_card_to_sensor(self):
        self._machine.set_message("Cannot pay because there is no printing paper")

    def press_printing_button(self):
        self._machine.set_message("Please call service for additional printing paper")

    def open_machine_and_add_printing_paper_pieces(self):
        self._machine.add_printing_paper_pieces(100)
        self._machine.state = MoneyMachineState.NEED_PAYMENT
class PaidState(ParkingTicketVendingMachineState):
    def __init__(self, machine):
        self._machine = machine

    def move_credit_card_to_sensor(self):
        self._machine.set_message("Alreay paid. Press button for printout")

    def press_printing_button(self):
        self._machine.print_ticket()
        if self._machine.get_printing_paper_pieces == 0:
            self._machine.state = MoneyMachineState.NO_PAPER
        else:
            self._machine.state = MoneyMachineState.NEED_PAYMENT

    def open_machine_and_add_printing_paper_pieces(self):
        self._machine.set_message("Only authorized personel can add paper")
class StillNeedToPayState(ParkingTicketVendingMachineState):
    def __init__(self, machine):
        self._machine = machine

    def move_credit_card_to_sensor(self):
        self._machine.pay_for_one_hour_with_credit_card()
        if self._machine.state == MoneyMachineState.NEED_PAYMENT:
            self._machine.state = MoneyMachineState.PAID_READY_TO_PRINT

    def press_printing_button(self):
        self._machine.set_message("You need to pay first")

    def open_machine_and_add_printing_paper_pieces(self):
        self._machine.set_message("Only authorized personel can add paper")
class UnavailableState(ParkingTicketVendingMachineState):
    def __init__(self, machine):
        self._machine = machine

    def move_credit_card_to_sensor(self):
        self._machine.set_message("Vending machine unavailable")

    def press_printing_button(self):
        self._machine.go_down()
        self._machine.state = MoneyMachineState.UNAVAILABLE

    def open_machine_and_add_printing_paper_pieces(self):
        self._machine.set_message("Vending machine unavailable")
def main():
    machine = ParkingTicketVendingMachine()
    state = StillNeedToPayState(machine)
    state.open_machine_and_add_printing_paper_pieces()
    state.press_printing_button()
    state.move_credit_card_to_sensor()

    state = PaidState(machine)
    state.move_credit_card_to_sensor()
    state.open_machine_and_add_printing_paper_pieces()
    state.press_printing_button()

if __name__ == '__main__':
    main()

Using the State pattern, the number of classes defined in the application can increase significantly, but ultimately we gain readability. If your application has many possible states that change frequently, use the State pattern.