본문 바로가기
Practical Python Design Patterns

State Pattern

by 자동매매 2023. 3. 29.

CHAPTER 15 State Pattern

Under pressure.

Queen, Under Pressure

A very useful tool for thinking through software problems is the state diagram. In a state diagram, you construct a graph, where nodes represent the state of the system and edges are transitions between one node in the system and another. State diagrams are useful because they let you think visually about the state of your system given certain inputs. You are also guided to consider the ways in which your system transitions from one state to the next.

Since we mentioned creating games earlier in this book, we can model the player character as a state machine. The player might start out in the standing state. Pressing the left or right arrow keys might change the state to moving left or moving right. The up arrow can then take the character from whatever state they re in into the jumping state, and, similarly, the down button will take the character into the crouching state. Even though this is an incomplete example, you should begin to understand how we might use a state diagram. It also becomes clear that when the up arrow key is pressed, the character will jump up and then come back down; if the key has not been released, the character will jump again. Releasing any key will take the character from whatever state it was in back to the standing state.

Another example from a more formal sphere would be looking at an ATM machine. A simplified state machine for an ATM machine might include the following states:

waiting

accepting card

accepting PIN input validating PIN

239

' Wessel Badenhorst 2017

W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_15
CHAPTER 15 STATE PATTERN

rejecting PIN

Getting transaction selection

A set of states for every part of every transaction finalizing transaction

returning card

printing slip

dispensing slip

Specific system and user actions will cause the ATM to move from one state to the next. Inserting a card into the machine will cause the machine to transition from the waiting state to the accepting_card state, and so on.

Once you have drawn up a complete state diagram for a system, you will have a fairly complete picture of what the system should be doing every step of the way. You will also have clarity on how your system should be moving from one step to another. All that remains is to translate your state diagram into code.

A naive way to translate the diagram into runnable code is to create an object that represents the state machine. The object will have an attribute for its state, which will determine how it reacts to input. If we were to code up the first example of the game character, it would look something like this.

Window systems have issues with curses, but that can be fixed by installing Cygwin, which gives you a Linux-like terminal that works well with the curses library curses.

To download Cygwin, head to the main website at https://www.cygwin.com/ and download the current DLL version that matches your machine. If you find you have

to set up Python again, just follow the steps under the Linux section of the installation guide from the beginning of this book.

import time import curses

def main():

win = curses.initscr() curses.noecho()

win.addstr(0, 0, "press the keys w a s d to initiate actions") win.addstr(1, 0, "press x to exit")

240
CHAPTER 15 STATE PATTERN

win.addstr(2, 0, "> ") win.move(2, 2)

while True:

ch = win.getch()

if ch is not None:

win.move(2, 0) win.deleteln()

win.addstr(2, 0, "> ")

if ch == 120:

break

elif ch == 97: # a

print("Running Left") elif ch == 100: # d

print("Running Right") elif ch == 119: # w

print("Jumping")

elif ch == 115: # a

print("Crouching")

else: print("Standing")

time.sleep(0.05)

if __name__ == "__main__":

main()

As an exercise, you can expand upon this code using the pygame module from chapter 4 to create a character that can run and jump on the screen instead of just printing out the action the character is currently taking. It might be useful to look at the pygame documentation to see how to load a sprite image from a file (sprite sheet) instead of just drawing a simple block on the screen.

By this time, you know that the if statement we use to determine what to do with

the input is a problem. It smells bad to your code sense. Since state diagrams are such useful representations of object-oriented systems, and the resulting state machines are widespread, you can be sure that there is a design pattern to clean up this code smell. The design pattern you are looking for is the state pattern.

241
CHAPTER 15 STATE PATTERN

State Pattern

On an abstract level, all object-oriented systems concern themselves with the actors in

a system and how the actions of each impact the other actors and the system as a whole. This is why a state machine is so helpful in modeling the state of an object and the things that cause said object to react.

In object-oriented systems, the state pattern is used to encapsulate behavior variations based on the internal state of an object. This encapsulation solves the monolithic conditional statement we saw in our previous example.

This sounds great, but how would this be accomplished?

What you need is some sort of representation for the state itself. Sometimes, you may even want all the states to share some generic functionality, which will be coded into

the State base class. Then, you have to create a concrete State class for every discrete state in your state machine. Each of these can have some sort of handler function to deal with input and cause a transition of state, as well as a function to complete the action required of that state.

In the following code snippet, we define an empty base class that the concrete State classes will be inherited from. Note that, as in previous chapters, Python s duck-typing system allows you to drop the State class in this most basic implementation. I include

the base class State in the snippet for clarity only. The concrete states also lack action methods, as this implementation takes no action, and as such these methods would be empty.

class State(object):

pass

class ConcreteState1(State):

def __init__(self, state_machine):

self.state_machine = state_machine

def switch_state(self):

self.state_machine.state = self.state_machine.state2

class ConcreteState2(State):

def __init__(self, state_machine):

self.state_machine = state_machine

242
CHAPTER 15 STATE PATTERN

def switch_state(self):

self.state_machine.state = self.state_machine.state1

class StateMachine(object):

def __init__(self):

self.state1 = ConcreteState1(self) self.state2 = ConcreteState2(self) self.state = self.state1

def switch(self):

self.state.switch_state()

def __str__(self):

return str(self.state)

def main():

state_machine = StateMachine() print(state_machine)

state_machine.switch() print(state_machine)

if __name__ == "__main__":

main()

From the result, you can see where the switch happens from ConcreteState1 to ConcreteState2. The StateMachine class represents the context of execution and provides a single interface to the outside world.

<__main__.ConcreteState1 object at 0x7f184f7a7198> <__main__.ConcreteState2 object at 0x7f184f7a71d0>

As you progress down the path to becoming a better programmer, you will find yourself spending more and more time thinking about how you can test certain code constructs. State machines are no different. So, what can we test about a state machine?

  1. That the state machine initializes correctly
  2. at the action method for each concrete State class does what it should do, like return the correct value

243
CHAPTER 15 STATE PATTERN

  1. at, for a given input, the machine transitions to the correct subsequent state
  2. Python includes a very solid unit-testing framework, not surprisingly called unittest.

To test our generic state machine, we could use the following code: import unittest

class GenericStatePatternTest(unittest.TestCase):

def setUp(self):

self.state_machine = StateMachine()

def tearDown(self):

pass

def test_state_machine_initializes_correctly(self):

self.assertIsInstance(self.state_machine.state, ConcreteState1)

def test_switch_from_state_1_to_state_2(self):

self.state_machine.switch()

self.assertIsInstance(self.state_machine.state, ConcreteState2)

def test_switch_from_state2_to_state1(self):

self.state_machine.switch() self.state_machine.switch()

self.assertIsInstance(self.state_machine.state, ConcreteState1)

if __name__ == ’__main__’:

unittest.main()

Play around with the options for asserts and see what other interesting tests you can come up with.

We will now return to the problem of the player character either running or walking, as we discussed in the beginning of this chapter. We will implement the same functionality from before, but this time we will use the state pattern.

244
CHAPTER 15 STATE PATTERN

import curses

import time

class State(object):

def __init__(self, state_machine):

self.state_machine = state_machine

def switch(self, in_key):

if in_key in self.state_machine.mapping:

self.state_machine.state = self.state_machine.mapping[in_key] else:

self.state_machine.state = self.state_machine. mapping["default"]

class Standing(State):

def __str__(self):

return "Standing"

class RunningLeft(State):

def __str__(self):

return "Running Left"

class RunningRight(State):

def __str__(self):

return "Running Right"

class Jumping(State):

def __str__(self):

return "Jumping"

class Crouching(State):

def __str__(self):

return "Crouching"

class StateMachine(object):

def __init__(self):

self.standing = Standing(self) self.running_left = RunningLeft(self) self.running_right = RunningRight(self)

245
CHAPTER 15 STATE PATTERN

self.jumping = Jumping(self) self.crouching = Crouching(self)

self.mapping = {

"a": self.running_left, "d": self.running_right, "s": self.crouching, "w": self.jumping, "default": self.standing,

}

self.state = self.standing

def action(self, in_key):

self.state.switch(in_key)

def __str__(self):

return str(self.state)

def main():

player1 = StateMachine() win = curses.initscr() curses.noecho()

win.addstr(0, 0, "press the keys w a s d to initiate actions") win.addstr(1, 0, "press x to exit")

win.addstr(2, 0, "> ")

win.move(2, 2)

while True:

ch = win.getch()

if ch is not None:

win.move(2, 0) win.deleteln() win.addstr(2, 0, "> ") if ch == 120:

break

246
CHAPTER 15 STATE PATTERN

player1.action(chr(ch))

print(player1.state) time.sleep(0.05)

if __name__ == "__main__":

main()

How do you feel about the altered code? What do you like about it? What have you learned? What do you think can be improved?

I want to encourage you to begin looking at code online and asking yourself these questions. You will often find you learn more from reading other people s code than from all the tutorials you could ever find online. That is also the point where you take another big step forward in your learning, when you begin to think critically about the code you find online rather than just copying it into your project and hoping for the best.

Parting Shots

In this chapter, we discovered how closely we could link actual code in Python to an abstract tool for solving multiple types of problems, namely the state machine.

All state machines are composed of states and the transitions taking the machine from one state to another based on certain inputs. Usually, the state machine will also execute some actions while in a state before transitioning to another state.

We also looked at the actual code you could use to build your very own state machine in Python.

Here are some quick and simple ideas for constructing your own state machine based solutions.

  1. Identify the states your machine can be in, such as running or walking or, in the case of a traffic light, red, yellow, and green.
  2. Identify the different inputs you expect for each state.
  3. Draw a transition from the current state to the next state based on the input. Note the input on the transition line.
  4. Define the actions taken by the machine in each state.
  5. Abstract the shared actions into the base State class.
  6. Implement concrete classes for each state you identified.

247
CHAPTER 15 STATE PATTERN

  1. Implement a set of transition methods to deal with the expected inputs for every state.
  2. Implement the actions that need to be taken by the machine in every state. Remember, these actions live in the concrete State class as well as in the base State class.

There you have it a fully implemented state machine that has a one-to-one relation to the abstract diagram solving the problem.

Exercises

Expand on the simple state machine for the player character by using

pygame to create a visual version. Load a sprite for the player and then model some basic physics for the jump action.

Explore the different types of assert statements available to you as

part of the Python unittest library.

248

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

Template Method Pattern  (0) 2023.03.29
Strategy Pattern  (0) 2023.03.29
Observer Pattern  (0) 2023.03.29
Iterator Pattern  (0) 2023.03.29
Interpreter Pattern  (0) 2023.03.28

댓글