'State' is a common programming term that is experienced by all developers as they advance from beginning to intermediate-level programming. So, what exactly does the term "State" mean?
In general, an object's state is merely the current snapshot of the object or a portion of it. Meanwhile, in computer science, a program's state is defined as its position about previously-stored inputs. In this context, the term "state" is used in the same manner as it is in science: the state of an object, such as a gas, liquid, or solid, represents its current physical nature, and the state of a computer program reflects its current values or contents.
The stored inputs are preserved as variables or constants in a computer program. While assessing the state of a program, developers might examine the values contained in these inputs. The state of the program may change while it runs - variables may change, and memory values may change. A control variable, such as one used in a loop, for example, changes the state of the program at each iteration. Examining the present state of a program may be used to test or analyze the codebase.
In simpler systems, state management is frequently handled with if-else, if-then-else, try-catch statements, or boolean flags; however, this is useless when there are too many states imaginable in a program. They can lead to clunky, complicated code that is difficult to understand, maintain, and debug.
One disadvantage of if-else-clauses or booleans is that they may get fairly extensive, and adding another state is difficult because it necessitates rewriting the code of many different classes. Assume you want to make a game that has the main menu, a game loop, and a completion screen.
Let's build a video player for example:
class Video:
def __init__(self, source):
self.source = source
self.is_playing = False
self.is_paused = False
self.is_stopped = True
# A video can only be played when paused or stopped
def play(self):
if not self.is_playing or self.is_paused:
# Make the call to play the video
self.is_playing = True
self.is_paused = False
else:
raise Exception(
'Cannot play a video that is already playing.'
)
# A video can only be paused when it is playing
def pause(self):
if self.is_playing:
# Make the call to pause the video
self.is_playing = False
self.is_paused = True
else:
raise Exception(
'Cannot pause a video that is not playing'
)
# A video can only be stopped when it is playing or paused
def stop(self):
if self.is_playing or self.is_paused:
# Make the call to stop the video
self.is_playing = False
self.is_paused = False
else:
raise Exception(
'Cannot stop a video that is not playing or paused'
)
The above code snippet is an if-else implementation of a simple video player application, where the three basic states are - playing, paused, and stopped. However, if we try to add more states, the code will rapidly become complex, bloated, repetitive, and hard to understand and test. Let’s see what the code looks like when adding another state ‘rewind’:
class Video:
def __init__(self, source):
self.source = source
self.is_playing = False
self.is_paused = False
self.is_rewinding = False
self.is_stopped = True
# A video can only be played when it is paused or stopped or rewinding
def play(self):
if self.is_paused or self.is_stopped or self.is_rewinding:
# Make the call to play the video
self.is_playing = True
self.is_paused = False
self.is_stopped = False
self.is_rewinding = False
else:
raise Exception(
'Cannot play a video that is already playing.'
)
# A video can only be paused when it is playing or rewinding
def pause(self):
if self.is_playing or self.is_rewinding:
# Make the call to pause the video
self.is_playing = False
self.is_paused = True
self.is_rewinding = False
self.is_stopped = False
else:
raise Exception(
'Cannot pause a video that is not playing or rewinding'
)
# A video can only be stopped when it is playing or paused or rewinding
def stop(self):
if self.is_playing or self.is_paused or self.is_rewinding:
# Make the call to stop the video
self.is_playing = False
self.is_paused = False
self.is_stopped = True
self.is_rewinding = False
else:
raise Exception(
'Cannot stop a video that is not playing or paused or rewinding'
)
# 4. A video can only be rewinded when it is playing or paused.
def rewind(self):
if self.is_playing or self.is_paused:
# Make the call to rewind the video
self.is_playing = False
self.is_paused = False
self.is_stopped = False
self.is_rewinding = True
else:
raise Exception(
'Cannot rewind a video that is not playing or paused'
)
Without the State-pattern, you'd have to examine the program's current state throughout the code, including the update and draw methods. If you want to add a fourth state, such as a settings screen, you'll have to update the code of many distinct classes, which is inconvenient. This is where the idea of state machines comes in handy.
State machines are not a novel concept in computer science; they are one of the basic design patterns utilized in the software business. It is more system-oriented than coding-oriented and is used to model around use cases.
Let's look at a simple real-life example of hiring a cab through Uber:
Screen 1 is the first screen that all users in this use case see, and it is self-contained. Screen 2 is reliant on Screen 1, and you will not be able to go to Screen 2 until you give accurate data on Screen 1. Likewise, screen 3 is dependent on screen 2, while screen 4 is dependent on screen 3. If neither you nor your driver cancels your trip, you'll be moved to screen 4, where you won't be able to plan another trip until your current one ends.
Let's say it's raining severely and no driver accepts your trip or no available driver in your region is found to finish your travel; an error notification warning you of driver unavailability shows, and you remain on screen 3. You may still return to screen 2, screen 1, and even the very first screen.
You are in a different step of the cab reservation process, and you may only go to the next level if a specified action in the current stage is successful. For example, if you input the wrong location on screen 1, you won't be able to proceed to screen 2, and you won't be able to proceed to screen 3 unless you pick a travel option on screen 2, but you may always return to the previous stage unless your trip is already booked.
In the above example, we've divided the cab booking process into several activities, each of which may or may not be authorized to call another activity based on the status of the booking. A state machine is used to model this. In principle, each of these stages/states should be autonomous, with one summoning the next only after the current one has been finished, successfully or otherwise.
In more technical words, the state machine enables us to split down a large complicated action into a succession of separate smaller activities, such as the cab booking activity in the preceding example.
Events connect smaller tasks, and shifting from one state to another is referred to as transition. We normally conduct some actions after changing from one state to another, such as creating a booking in the back end, issuing an invoice, saving user analytics data, capturing booking data in a database, triggering payment after the trip is finished, and so on.
Hence, the general formula for a state machine can be given as:
Current State + Some Action / Event= Another State
Let’s see what a state machine designed for a simple video player application would look like:
And we can implement it in code using transitions as follows:
from transitions import Machine
class Video:
# Define the states
PLAYING = 'playing'
PAUSED = 'paused'
STOPPED = 'stopped'
def __init__(self, source):
self.source = source
# Define the transitions
transitions = [
# 1. A video can only be played when it is paused or stopped.
{'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING},
{'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING},
# 2. A video can only be paused when it is playing.
{'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED},
# 3. A video can only be stopped when it is playing or paused.
{'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED},
{'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED},
]
# Create the state machine
self.machine = Machine{
model = self,
transitions = transitions,
initial = self.STOPPED
}
def play(self):
pass
def pause(self):
pass
def stop(self):
pass
Now, in case, we want to add another state, say rewind, we can do that easily as follows:
from transitions import Machine
class Video:
# Define the states
PLAYING = 'playing'
PAUSED = 'paused'
STOPPED = 'stopped'
REWINDING = 'rewinding' # new
def __init__(self, source):
self.source = source
# Define the transitions
transitions = [
# 1. A video can only be played when it is paused or stopped.
{'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING},
{'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING},
{'trigger': 'play', 'source': self.REWINDING, 'dest': self.PLAYING}, # new
# 2. A video can only be paused when it is playing.
{'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED},
{'trigger': 'pause', 'source': self.REWINDING, 'dest': self.PAUSED}, # new
# 3. A video can only be stopped when it is playing or paused.
{'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED},
{'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED},
{'trigger': 'stop', 'source': self.REWINDING, 'dest': self.STOPPED}, # new
# 4. A video can only be rewinded when it is playing or paused.
{'trigger': 'rewind', 'source': self.PLAYING, 'dest': self.REWINDING}, #new
{'trigger': 'rewind', 'source': self.PAUSED, 'dest': self.REWINDING}, # new
]
# Create the state machine
self.machine = Machine{
model = self,
transitions = transitions,
initial = self.STOPPED
}
def play(self):
pass
def pause(self):
pass
def stop(self):
pass
def rewind(self):
pass
Thus, we can see how state machines can simplify a complex implementation and save us from writing incorrect code. Having learned the capabilities of state machines, it’s now important to understand why and when to use state machines.
State Machines can be utilized in applications that have distinct states. Each stage can lead to one or more subsequent states, as well as end the process flow. A State Machine employs user input or in-state computations to choose which state to enter next.
Many applications necessitate a "initialize" stage, followed by a default state that allows for a wide range of actions. Previous and present inputs, as well as states, can all have an impact on the actions that are executed. Clean-up measures can then be carried out when the system is "shut down."
A state machine can help us conceptualize and manage those units more abstractly if we can break down a hugely complex job into smaller, independent units, where we simply need to describe when a state can transition to another state and what happens when the transition occurs. We don't need to be concerned with how the transition occurs after setup. After that, we only need to think about when and what, not how.
Furthermore, state machines let us see the entire state process in a very predictable way; once transitions are set, we don't have to worry about mismanagement or erroneous state transitions; the improper transition may occur only if the state machine is configured properly. We have a comprehensive view of all states and transitions in a state machine.
If we don't use a state machine, we are either unable to visualize our systems in various possible states, or we are knowingly or unknowingly coupling our components tightly together, or we are writing many if-else conditions to simulate state transitions, which complicates unit and integration testing because we must ensure that all test cases are written to validate the possibility of all the conditions and branching used.
State machines, in addition to their ability to develop decision-making algorithms, are functional forms of application planning. As applications get more complex, the need for effective design grows.
State diagrams and flowcharts are useful and occasionally essential throughout the design process. State Machines are important not just for application planning, but are also simple to create.
Following are some of the major advantages of state machines in modern-day computing:
Not everything about state machines is good, they can sometimes lead to drawbacks and challenges too. Here are some of the common problems with state machines:
When using a state machine, your system should ideally have two logical components:
The state machine may be thought of as the infrastructure that drives state transitions; it verifies state transitions and executes configured actions before, during, and after a transition; however, it should not know what business logic is done in those actions.
So, in general, it's a good idea to isolate the state machine from the core business logic by using correct abstractions; otherwise, managing the code would be a nightmare.
Here are some other real-life scenarios where we need to employ state machine logic with caution:
Following are some of the practical applications that benefit from the concept of state machines in our daily lives:
When you purchase something from an online e-commerce site, for example, it goes through various phases, such as Ordered, Packed, Shipped, Cancelled, Delivered, Paid, Refunded, and so on. The transition occurs automatically as things move through a warehouse or logistics center and are scanned at various stages, such as when a user cancels or wants a refund.
The notion of state machines is extremely useful in programming. It not only streamlines the process of developing more complicated use case apps but also reduces the development work necessary. It provides a more simple and elegant grasp of modern-day events and when correctly applied, may work miracles.