Let’s write an infinitely long essay! But that’s an impossible task. However, we can create a process that, if run forever, would generate an infinitely long essay. Close enough.
Now, you can obviously generate a long and repetitive essay with a single line of Python code:
>>> "This is water. " * 20
'This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. '
Yaawn… boooring! Instead, we will generate a much more interesting essay using the State design pattern in this article.
First, we will understand what state machines are and how they’re related to the State design pattern. Next, we will create a state machine that can generate a (reasonably) interesting and infinite essay. Then, we will briefly go over the state design pattern. Finally, we will translate that state machine into object-oriented code using the state design pattern.
Software design patterns are effective ways to solve commonly occurring problems. When applied appropriately, software design patterns like the state pattern can help you write better scalable, maintainable, and testable software.
In essence, the State design pattern translates a State Machine into object-oriented code.
If you aren’t familiar with state machines, it’s a pretty simple concept. A state machine has states and transitions. States are certain properties of our system of interest, and state transitions are actions that change these properties and thereby also cause a state change.
Since I have a Robotics background (among other things) and because state machines are used extensively in Robotics, I’ll use a simple example of a robot vacuum cleaner to illustrate how state machines work.
The state machine diagram paints an intuitive picture of how the robot operates, even if you’ve never encountered state machines. Let’s go over this operation step by step.
Thus, our robot vacuum cleaner has three states — Docked, Cleaning, and Charging — and has transitions based on sensory detection of the floor and its battery.
Now that we understand state machines at a basic level let’s create a state machine capable of writing an infinite essay.
Above is a state machine that uses English grammar to generate an essay comprised of short, simple sentences. I promise we’ll get to a more interesting version very soon, but this should serve as a good starting point for understanding. Let’s go over how it works.
This state machine might generate a (nonsensical) essay that looks like this.
The World barks red! Cousin Harry rains foul? The tigers shimmer fun. …
Although “non-deterministic” sounds complicated, for our purposes, it just means adding in some randomness. Essentially we’re adding a kind of a coin toss before transitioning to some of the states. You’ll see what I mean.
The non-deterministic state machine above is very similar to the one before. The only differences are:
With the introduction of randomness, negation, and conjunctions, we can now generate more interesting and variable-length sentences.
Now, let’s understand how the state design pattern works. Again, remember that we’re trying to translate a state machine to object-oriented code.
In the essay generation state machine, observe that every state needs to do two things.
From the point of view of a particular state, there is nothing else that it needs to know about or do. Instead of being bogged down by the complexity of the entire system — all its states and transitions — we can just focus on one state at a time. In my view, this kind of isolation and decoupling is the biggest selling point of the State pattern.
Below, we have a UML diagram for the State design pattern. There is some context in which each of the states operates, illustrated by the Context
class. The context object has a private state attribute, which it uses to call the current state to perform its action. Each state implements a State
interface with methods for performing its action or operation and returning the next state.
If we map this onto the essay generation example, the UML diagram looks like this.
WordState
is now an abstract class (indicated by italics) instead of an interface. Abstract classes can have some abstract (not implemented) methods and attributes, while others can be defined. Interfaces are totally abstract: all their methods are abstract. I made this change because generateWord
implementation is the same across all the states, and avoiding duplicate code is good.
Let’s break down each of the attributes and methods above. In the EssayContext
class, we have:
state
: Reference to the current WordState
object.essayBody
: List of all the words generated so far.setState()
: Setter to change the state
attribute.addWord()
: Method to add the next word to the essay body.generateEssay()
: We call this method to generate our essay; we stop when the essayBody
has length greater than length
.printEssay()
: Returns a string of the generated essay.
In the abstract class WordState
, we have:
wordList
: Abstract property (indicated by italics) for a list of words from which we choose words to generate.generateWord()
: Method which adds generated word to the essay context.nextState()
: Abstract method for returning the next state.
We’ll use NounState
as a representative example for all the other concrete states inherited from WordState
.
wordList
: A list of nouns from which we choose words to generate.nextState()
: Returns the next state.
Now, we have everything we need to actually implement this in code. Let’s go ahead and do just that!
Let’s first write the EssayContext
class in a file called essay_context.py
. We'll ditch camel case and switch to snake case because, well, Python is a... snake (sorry).
from word_state import WordState
class EssayContext:
def __init__(self, state: WordState):
self.state = state
self.essay_body: list[str] = []
def set_state(self, state: WordState):
self.state = state
def add_word(self, word: str):
self.essay_body.append(word)
def generate_essay(self, length: int):
while len(self.essay_body) < length:
self.state.generate_word(self)
def print_essay(self) -> str:
return " ".join(self.essay_body)
Then, let’s add the states in a file called word_state.py
.
import abc
import numpy as np
class WordState(abc.ABC):
word_list: list[str]
@classmethod
def generate_word(cls, context: "EssayContext"):
word = np.random.choice(cls.word_list)
context.add_word(word)
context.set_state(cls.next_state())
@classmethod
@abc.abstractmethod
def next_state(cls) -> "WordState":
pass
class NounState(WordState):
word_list: list[str] = ["everything", "nothing"]
@classmethod
def next_state(cls):
return VerbState
class VerbState(WordState):
word_list: list[str] = ["is", "was", "will be"]
@classmethod
def next_state(cls):
heads = np.random.rand() < 0.5
if heads:
return NegationState
return AdjectiveState
class NegationState(WordState):
word_list: list[str] = ["not"]
@classmethod
def next_state(cls):
return AdjectiveState
class AdjectiveState(WordState):
word_list: list[str] = ["fantastic", "terrible"]
@classmethod
def next_state(cls):
heads = np.random.rand() < 0.5
if heads:
return ConjunctionState
return EndmarkState
class ConjunctionState(WordState):
word_list: list[str] = ["and", "but"]
@classmethod
def next_state(cls):
return NounState
class EndmarkState(WordState):
word_list: list[str] = [".", "!"]
@classmethod
def next_state(cls):
return NounState
Finally, let’s add code to run everything in main.py
.
from essay_context import EssayContext
from word_state import NounState
if __name__ == '__main__':
ctx = EssayContext(NounState)
ctx.generate_essay(100)
print(ctx.print_essay())
Running python main.py
gives us the following output (different each time because of non-determinism):
'everything is not terrible and nothing was terrible ! everything will be not fantastic but everything is fantastic . everything will be fantastic . nothing will be fantastic and nothing will be terrible ! everything was not fantastic and everything will be not terrible . everything was terrible . nothing was terrible but nothing will be fantastic ! nothing is not terrible . nothing was not fantastic but everything was not fantastic ! everything will be not fantastic but everything will be terrible ! everything will be not fantastic . everything is fantastic but nothing will be not terrible ! everything will be not fantastic but nothing was not fantastic !'
Not bad for such a simple system! We can also extend the various word lists or add more states to make the essay generation more sophisticated. We could even introduce some LLM APIs to take our essays to the next level.
State machines and the State pattern are a great fit to model and create systems with a well-defined notion of a “state.” That is, there are specific behaviors and properties associated with each state. The robot vacuum cleaner is cleaning, docked or charging. Your TV can be ON or OFF, and the TV remote buttons will act differently based on the TV’s state.
It’s also a good fit for generating or identifying sequences with a well-defined pattern. This applies to our essay generation example.
Finally, you might ask, “What’s the point of all this?” Why did we go through all the trouble defining the various states and classes to generate this “infinite” essay? We could have written 20 (or fewer) lines of Python code to achieve the same behavior.
The short answer is for better scalability.
Imagine if, instead of just three or five states, we had 50 or 500 states. This isn’t hyperbole; real enterprise systems do have that level of complexity. Suddenly, the State pattern seems much more appealing because of its decoupling and isolation. We can just focus on one state at a time without having to keep the entire system in our heads. It’s easier to introduce changes since one state won’t affect others. It also allows for easier unit testing, a big part of a scalable and maintainable system.
Ultimately, the State pattern is not just about managing states; like all design patterns, it’s a blueprint for building systems that are as scalable and maintainable as they are sophisticated.