본문 바로가기
Mastering Python Design Patterns

The State Pattern

by 자동매매 2023. 3. 22.

The State Pattern

In the previous chapter, we covered the Observer pattern, which is useful in a program to notify other objects when the state of a given object changes. Let's continue discovering those patterns proposed by the Gang of Four.

Object-oriented programming (OOP) focuses on maintaining the states of objects that interact with each other. A very handy tool to model state transitions when solving many problems is known as a finite-state machine (commonly called a state machine).

What's a state machine? A state machine is an abstract machine that has two key components, that is, states and transitions. A state is the current (active) status of a system. For example, if we have a radio receiver, two possible states for it are to be tuned to FM or AM. Another possible state is for it to be switching from one FM/AM radio station to another. A transition is a switch from one state to another. A transition is initiated by a triggering event or condition. Usually, an action or set of actions is executed before or after a transition occurs. Assuming that our radio receiver is tuned to the 107 FM station, an example of a transition is for the button to be pressed by the listener to switch it to 107.5 FM.

A nice feature of state machines is that they can be represented as graphs (called state diagrams), where each state is a node and each transition is an edge between two nodes.

State machines can be used to solve many kinds of problems, both non-computational and computational. Non-computational examples include vending machines, elevators, traffic lights, combination locks, parking meters, and automated gas pumps. Computational examples include game programming and other categories of computer programming, hardware design, protocol design, and programming language parsing.

Now we have an idea of what state machines are! But, how are state machines related to the State design pattern? It turns out that the State pattern is nothing more than a state machine applied to a particular software engineering problem [Gang of Four-95 book, page 342], [Python 3 Patterns, Recipes and Idioms by Bruce Eckel-08, page 151].

The State Pattern Chapter 142*

In this chapter, we will discuss:

  • Real-world examples
    • Use cases
      • Implementation

Real-world examples

A snack vending machine is an example of the State pattern in everyday life. Vending machines have different states and react differently depending on the amount of money that we insert. Depending on our selection and the money we insert, the machine can do the following:

  • Reject our selection because the product we requested is out of stock
    • Reject our selection because the amount of money we inserted was not sufficient
      • Deliver the product and give no change because we inserted the exact amount
        • Deliver the product and return the change

There are, for sure, more possible states, but you get the point.

In the software category, we can think of the following examples:

  • The django-fsm package is a third-party package that can be used to simplify the implementation and usage of state machines in the Django Framework (j.mp/django-fsm).
    • Python offers more than one third-party package/module to use and implement state machines (j.mp/pyfsm). We will see how to use one of them in the implementation section.
      • The State Machine Compiler (SMC) project (http:/ / smc. sourceforge.net). With SMC, you can describe your state machine in a single-text file using a simple domain-specific language (DSL), and it will generate the state machine's code automatically. The project claims that the DSL is so simple that you can write it as a one-to-one translation of a state diagram. I haven't tried it, but it sounds very interesting. SMC can generate code in a number of programming languages, including Python.

Use cases

The State pattern is applicable to many problems. All the problems that can be solved using state machines are good use cases for using the State pattern. An example we have already seen is the process model for an operating/embedded system.

Programming language compiler implementation is another good example. Lexical and syntactic analysis can use states to build abstract syntax trees.

Event-driven systems are yet another example. In an event-driven system, the transition from one state to another triggers an event/message. Many computer games use this technique. For example, a monster might move from the guard state to the attack

state when the main hero approaches it.

To quote Thomas Jaeger:

*"The state design pattern allows for full encapsulation of an unlimited number of states on a context for easy maintenance and flexibility."*

Implementation

Let's write code that demonstrates how to create a state machine based on the state diagram shown earlier in this chapter. Our state machine should cover the different states of a process and the transitions between them.

The State design pattern is usually implemented using a parent State class that contains

the common functionality of all the states, and a number of concrete classes derived from State, where each derived class contains only the state-specific required functionality. In my opinion, these are implementation details. The State pattern focuses on implementing a state machine. The core parts of a state machine are the states and transitions between the states. It doesn't matter how those parts are implemented.

To avoid reinventing the wheel, we can make use of the existing Python modules that not only help us create state machines, but also do it in a Pythonic way. A module that I find very useful is state_machine. Before going any further, if state_machine is not already installed on your system, you can install it using the pip install state_machine

command.

The state_machine module is simple enough that no special introduction is required. We will cover most aspects of it while going through the code of the example.

Let's start with the Process class. Each created process has its own state machine. The first step to create a state machine using the state_machine module is to use the @acts_as_state_machine decorator:

@acts_as_state_machine class Process:

Then, we define the states of our state machine. This is a one-to-one mapping of what we see in the state diagram. The only difference is that we should give a hint about the initial state of the state machine. We do that by setting the initial attribute value to True:

created = State(initial=True) waiting = State()

running = State()

terminated = State()

blocked = State()

swapped_out_waiting = State() swapped_out_blocked = State()

Next, we are going to define the transitions. In the state_machine module, a transition is an instance of the Event class. We define the possible transitions using the arguments from_states and to_state:

wait = Event(from_states=(created, running, blocked, swapped_out_waiting), to_state=waiting)

run = Event(from_states=waiting, to_state=running)

terminate = Event(from_states=running, to_state=terminated)

block = Event(from_states=(running, swapped_out_blocked), to_state=blocked)

swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting) swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)

Also, as you may have noted, from_states can be either a single state or a group of states (tuple).

Each process has a name. Officially, a process needs to have much more information to be useful (for example, ID, priority, status, and so forth) but let's keep it simple to focus on the pattern:

def __init__(self, name): self.name = name

Transitions are not very useful if nothing happens when they occur. The state_machine module provides us with the @before and @after decorators that can be used to execute actions before or after a transition occurs, respectively. You can imagine updating some object(s) within the system or sending an email or a notification to someone. For the purpose of this example, the actions are limited to printing information about the state change of the process, as follows:

@after('wait')

def wait_info(self):

print(f'{self.name} entered waiting mode')

@after('run')

def run_info(self):

print(f'{self.name} is running')

@before('terminate')

def terminate_info(self):

print(f'{self.name} terminated')

@after('block')

def block_info(self):

print(f'{self.name} is blocked')

@after('swap_wait')

def swap_wait_info(self):

print(f'{self.name} is swapped out and waiting')

@after('swap_block')

def swap_block_info(self):

print(f'{self.name} is swapped out and blocked')

Next, we need the transition() function, which accepts three arguments:

  • process, which is an instance of Process
    • event, which is an instance of Event (wait, run, terminate, and so forth)
      • event_name, which is the name of the event

And the name of the event is printed if something goes wrong when trying to execute event.

Here is the code for the function:

def transition(process, event, event_name):

try:

event()

except InvalidStateTransition as err:

print(f'Error: transition of {process.name}

from {process.current_state} to {event_name} failed')

The state_info() function shows some basic information about the current (active) state of the process:

def state_info(process):

print(f'state of {process.name}: {process.current_state}') At the beginning of the main() function, we define some string constants, which are

passed as event_name:

def main():

RUNNING = 'running'

WAITING = 'waiting'

BLOCKED = 'blocked'

TERMINATED = 'terminated'

Next, we create two Process instances and display information about their initial state:

p1, p2 = Process('process1'), Process('process2') [state_info(p) for p in (p1, p2)]

The rest of the function experiments with different transitions. Recall the state diagram we covered in this chapter. The allowed transitions should be with respect to the state diagram. For example, it should be possible to switch from a running state to a blocked state, but it shouldn't be possible to switch from a blocked state to a running state:

print()

transition(p1, p1.wait, WAITING)

transition(p2, p2.terminate, TERMINATED)

[state_info(p) for p in (p1, p2)]

print()

transition(p1, p1.run, RUNNING)

transition(p2, p2.wait, WAITING)

[state_info(p) for p in (p1, p2)]

print()

transition(p2, p2.run, RUNNING)

[state_info(p) for p in (p1, p2)]

print()

[transition(p, p.block, BLOCKED) for p in (p1, p2)] [state_info(p) for p in (p1, p2)]

print()

[transition(p, p.terminate, TERMINATED) for p in (p1, p2)] [state_info(p) for p in (p1, p2)]

Here is the full code of the example (the state.py file):

  1. We begin by importing what we need from state_machine:

from state_machine import (State, Event, acts_as_state_machine,

after, before, InvalidStateTransition)

  1. We define the Process class with its simple attributes:

@acts_as_state_machine

class Process:

created = State(initial=True)

waiting = State()

running = State()

terminated = State()

blocked = State()

swapped_out_waiting = State()

swapped_out_blocked = State()

wait = Event(from_states=(created, running, blocked, swapped_out_waiting), to_state=waiting)

run = Event(from_states=waiting, to_state=running)

terminate = Event(from_states=running, to_state=terminated)

block = Event(from_states=(running, swapped_out_blocked), to_state=blocked)

swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting) swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)

  1. We add the Process class's initialization method:

def __init__(self, name):

self.name = name

  1. We also need to define, on the Process class, the methods to provide its states:

@after('wait')

def wait_info(self):

print(f'{self.name} entered waiting mode') @after('run')

def run_info(self):

print(f'{self.name} is running') @before('terminate')

def terminate_info(self):

print(f'{self.name} terminated') @after('block')

def block_info(self):

print(f'{self.name} is blocked') @after('swap_wait')

def swap_wait_info(self):

print(f'{self.name} is swapped out and waiting') @after('swap_block')

def swap_block_info(self):

print(f'{self.name} is swapped out and blocked')

  1. We define the transition() function:

def transition(process, event, event_name):

try:

event()

except InvalidStateTransition as err:

print(f'Error: transition of {process.name}

from {process.current_state} to {event_name} failed')

  1. Next, we define the state_info() function:

def state_info(process):

print(f'state of {process.name}: {process.current_state}')

  1. Finally, here is the main part of the program:

def main():

RUNNING = 'running'

WAITING = 'waiting'

BLOCKED = 'blocked'

TERMINATED = 'terminated'

p1, p2 = Process('process1'), Process('process2') [state_info(p) for p in (p1, p2)]

print()

transition(p1, p1.wait, WAITING)

transition(p2, p2.terminate, TERMINATED)

[state_info(p) for p in (p1, p2)]

print()

transition(p1, p1.run, RUNNING)

transition(p2, p2.wait, WAITING)

[state_info(p) for p in (p1, p2)]

print()

transition(p2, p2.run, RUNNING)

[state_info(p) for p in (p1, p2)]

print()

[transition(p, p.block, BLOCKED) for p in (p1, p2)]

[state_info(p) for p in (p1, p2)]

print()

[transition(p, p.terminate, TERMINATED) for p in (p1, p2)] [state_info(p) for p in (p1, p2)]

if __name__ == '__main__':

main()

Here's what we get when executing python state.py:

Indeed, the output shows that illegal transitions such as created → terminated and blocked → terminated fail gracefully. We don't want the application to crash when an illegal transition is requested, and this is handled properly by the except block.

Notice how using a good module such as state_machine eliminates conditional logic. There's no need to use long and error-prone if...else statements that check for each and every state transition and react to them.

To get a better feeling for the State pattern and state machines, I strongly recommend you implement your own example. This can be anything: a simple video game (you can use state machines to handle the states of the main hero and the enemies), an elevator, a parser, or any other system that can be modeled using state machines.

Summary

In this chapter, we covered the State design pattern. The State pattern is an implementation of one or more finite-state machines (in short, state machines) used to solve a particular software-engineering problem.

A state machine is an abstract machine with two main components: states and transitions. A state is the current status of a system. A state machine can have only one active state at any point in time. A transition is a switch from the current state to a new state. It is normal to execute one or more actions before or after a transition occurs. State machines can be represented visually using state diagrams.

State machines are used to solve many computational and non-computational problems. Some of them are traffic lights, parking meters, hardware designing, programming language parsing, and so forth. We saw how a snack vending machine relates to the way a state machine works.

Modern software offers libraries/modules to make the implementation and usage of state machines easier. Django offers the third-party django-fsm package and Python also has many contributed modules. In fact, one of them (state_machine) was used in the implementation section. The State Machine Compiler is yet another promising project, offering many programming language bindings (including Python).

We saw how to implement a state machine for a computer system process using the state_machine module. The state_machine module simplifies the creation of a state machine and the definition of actions before/after transitions.

In the next chapter, we will discuss other behavioral design patterns: Interpreter, Strategy, Mememto, Iterator, and Template.

[ 142 ])

  • 3

'Mastering Python Design Patterns' 카테고리의 다른 글

The Observer Pattern in Reactive Programming  (0) 2023.03.22
Other Behavioral Patterns  (0) 2023.03.22
The Observer Pattern  (0) 2023.03.22
The Command Pattern  (0) 2023.03.22
The Chain of Responsibility  (0) 2023.03.22

댓글