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.