Design patterns are conventional solutions to common challenges in software development using Object-oriented programming. One of the famous design patterns for developing scalable and flexible software is the state pattern. You'll learn about the state pattern and how to apply it to improve your software project in this article.
Finite State Machine
Let's define a finite state machine (FSM) before moving on to the state design pattern. It is well known that the concepts of state patterns and finite-state machines have a close relationship. An FSM is something that behaves differently depending on its internal state. In computer programming, the behavior of an object in an application varies depending on its state. A switch and a lightbulb are simple examples of FSM. "ON" and "OFF" are the two possible states of the light bulb. To change the state of the bulb 'ON' or 'OFF', simply flip the switch. Transition is the process of moving from one state to another. Transition is affected by several factors. In the case of the light bulb, it is dependent on the input from the switch. The state diagram, which is shown below, graphically depicts the states and transitions.
We can implement the state machine using any programming language. Depending on a few factors, our code behaves differently. You can implement the preceding light bulb example as follows:
class LightBulb: _state = 'OFF' # initial state of bulb def onOff(self, switch): if switch == 'ON': self._state = 'ON' elif switch == 'OFF': self._state = 'OFF' else: continue # if we get wrong input
For small systems, such as the one described above, the code appears to be straightforward and simple. However, if there are many states and transitions, our code will get flabby with conditional statements. The code becomes more extensive, and it won't be easy to maintain the application. If you want to add additional states or transitions to the program, you'll have to change the entire code base. You can use the State Design Pattern in these cases.
State Pattern
It is a behavioral design pattern. You can use the state pattern to implement state-specific behavior in which objects can change their functionality at runtime. You can use it to avoid using conditional statements when changing an object's behavior based on its state. In the state pattern, you should encapsulate) different states in separate
State
classes. The original class keeps a reference to a state object based on its current state rather than using conditional statements to implement state-dependent functionality.UML Diagram
1) Context - it is the original class of our application. It maintains a reference to one of the concrete states on which its behavior depends. It also has a method to modify the internal state.
2) State interface - All supported states share the same state interface. Only the state interface allows Context to communicate with state objects. Context can only communicate with state objects via the state interface.
3) Concrete states - For each state, these objects implement the 'State' interface. These are the main objects which contain the state-specific methods.
How does it work?
Assume the Context is configured with an initial state,
concreteStateA
. It behaves according to its initial state. The Context now implements the doSomething
method according to concreteStateA
. The concrete states should contain a back reference to call back and change Context's current state object. If a state transition occurs, the Context's setSate
method is invoked, referencing the new state, concreteStateB
. The Context changed its internal state and behavior. Now, it uses the concreteStateB
to implement the doSomething
method. The basic idea is that the states can change the context's state automatically. As a developer, you can modify the states by using any number of instances of setState
.If you want to add another state, simply create a new concrete state object without changing the Context of the application.
Implementation
Let's take a step-by-step look at implementing the state pattern.
1) Find an existing class that contains state-dependent code, or create a suitable context class. It should include a reference to a specific state as well as a method for switching between states.
from __future__ import annotations from abc import ABC, abstractmethod # the context class contains a _state that references the concrete state and setState method to change between states. class Context: _state = None def __init__(self, state: State) -> None: self.setState(state) def setState(self, state: State): print(f"Context: Transitioning to {type(state).__name__}") self._state = state self._state.context = self def doSomething(self): self._state.doSomething()
2) Create a common State interface for all concrete states. The State interface specifies all of the methods that all Concrete States must implement and a backreference to the Context object. States can change the Context to another state by using this backreference.
class State(ABC): @property def context(self) -> Context: return self._context @context.setter def context(self, context: Context) -> None: self._context = context @abstractmethod def doSomething(self) -> None: pass
You should define the Context as a protected parameter. Above,
decorator is used to make the @property
context()
method as property and @context.setter
decorator to another overload of the context()
method as property setter method. Now, _context
is protected.3) You can define the concrete states in the classes that implement the state interface. After the
doSomething
method is called, the state of the Context changes. You can also change the state by defining a specific method. The state transitions use the setState
method of the Context.class ConcreteStateA(State): def doSomething(self) -> None: print("The context is in the state of ConcreteStateA.") print("ConcreteStateA now changes the state of the context.") self.context.setState(ConcreteStateB()) class ConcreteStateB(State): def doSomething(self) -> None: print("The context is in the state of ConcreteStateB.") print("ConcreteStateB wants to change the state of the context.") self.context.setState(ConcreteStateA())
4) You can now initiate your application with an initial state and execute the methods.
# sample application app = Context(ConcreteStateA()) app.doSomething() # this method is executed as in state 1 app.doSomething() # this method is executed as in state 2
The output of the above code looks something like this:
Context: Transitioning to ConcreteStateA The context is in the state of ConcreteStateA. ConcreteStateA now changes the state of the context. Context: Transitioning to ConcreteStateB The context is in the state of ConcreteStateB. ConcreteStateB wants to change the state of the context. Context: Transitioning to ConcreteStateA
Example
Let's create a simple state machine that represents a real-world scenario. Consider an elevator system with buttons in the elevator cabin that allow you to go up or down. Consider that this lift only travels between two floors to keep things simple. There are primarily two possible states for the elevator:
1st floor
and 2nd floor
. The input from the two buttons determines the transition between states. The elevator performs different actions based on its state.The following code is the implementation of the elevator example. Follow along with the comments for more descriptions about each method.
from __future__ import annotations from abc import ABC, abstractmethod # The Elevator class is the context. It should be initiated with a default state. class Elevator: _state = None def __init__(self, state: State) -> None: self.setElevator(state) # method to change the state of the object def setElevator(self, state: State): self._state = state self._state.elevator = self def presentState(self): print(f"Elevator is in {type(self._state).__name__}") # the methods for executing the elevator functionality. These depends on the current state of the object. def pushDownBtn(self): self._state.pushDownBtn() def pushUpBtn(self): self._state.pushUpBtn() # if both the buttons are pushed at a time, nothing should happen def pushUpAndDownBtns(self) -> None: print("Oops.. you should press one button at a time") # if no button was pushed, it should just wait open for guests def noBtnPushed(self) -> None: print("Press any button. Up or Down") # The common state interface for all the states class State(ABC): @property def elevator(self) -> Elevator: return self._elevator @elevator.setter def elevator(self, elevator: Elevator) -> None: self._elevator = elevator @abstractmethod def pushDownBtn(self) -> None: pass @abstractmethod def pushUpBtn(self) -> None: pass # The concrete states # We have two states of the elevator: when it is on the First floor and the Second floor class firstFloor(State): # If the down button is pushed when it is already on the first floor, nothing should happen def pushDownBtn(self) -> None: print("Already in the bottom floor") # if up button is pushed, move upwards then it changes its state to second floor. def pushUpBtn(self) -> None: print("Elevator moving upward one floor.") self.elevator.setElevator(secondFloor()) class secondFloor(State): # if down button is pushed it should move one floor down and open the door def pushDownBtn(self) -> None: print("Elevator moving down a floor...") self.elevator.setElevator(firstFloor()) # if up button is pushed nothing should happen def pushUpBtn(self) -> None: print("Already in the top floor") if __name__ == "__main__": # The client code. myElevator = Elevator(firstFloor()) myElevator.presentState() # Up button is pushed myElevator.pushUpBtn() myElevator.presentState()
The output of the above code looks like this:
Elevator is in firstFloor Elevator moving upward one floor. Elevator is in secondFloor
You can implement many buttons and states elevator like in real-life one for the floor. Try using the state pattern to implement the light bulb example from the Finite state machine.
Advantages and Disadvantages
The state pattern, like any other programming concept, has a number of benefits as well as some drawbacks. You can avoid writing massive conditional blocks for switching between states by using the state pattern instead of hard-coding state-specific behavior. It allows you to develop a flexible and maintainable application. You can add new states and transitions to the Context without changing it.
It's a good idea to use the state pattern if the logic of each state is complex and the states change frequently. Otherwise, it complicates simple things by bringing a plethora of classes and objects. The state pattern adds another level of indirection by imposing clients to rely on a State object, and it extends the context class to allow State objects to change the state of the Context.
Conclusion
In this article, you learned how to use the state pattern in Python programming to design state machines. Without using larger conditional blocks to implement state-specific behavior, the state pattern makes the development process a lot easier. You can also add new states that are not dependent on other states, giving your application more flexibility. The state pattern is very similar to the strategy pattern, which alters strategies based on user choices. The main distinction is that concrete states are aware of other states, whereas the strategies do not. Why do we say that states are aware of other states? because each state has to know to which state they should move. For example, the first-floor state knows that they should change to the second-floor state. Another important difference with strategy pattern is that in the case of Strategy pattern, it's the client, which provides a different strategy to Context, on State pattern, the state transition is managed by Context or State itself. Try to make use of the state pattern in your software to make the development process go more smoothly.
About the author
Giridhar Talla
Creative Web Developer