21
NovUnderstanding State Design Pattern
State Design Pattern
State Design Pattern is a fundamental concept in software design. It allows an object to alter its behavior when its internal state changes, making the object appear to change its class. This promotes flexibility by encapsulating state-specific behavior and transitions, improving code maintainability, and making the system more scalable.
In this design pattern tutorial, we will explore the State Design Pattern including "What is the State Design Pattern?", "When should I use the State Design Pattern?". We will also examine examples of the State Design Pattern in action. So, let's start with "What is the State Design Pattern?"
What is a State Design Pattern?
- The State Design Pattern is a behavioral design pattern that enables an object to adapt its behavior when its internal state changes.
- This pattern is especially effective when an object's functionality is dependent on its state, which can vary over the object's lifecycle.
This pattern seems like a dynamic version of the Strategy pattern.
State Design Pattern Benefits
- The advantages of employing the State pattern to implement polymorphic behavior are easy to see.
- The odds of error are reduced, and it's simple to add extra states for additional behavior.
- This makes our code more resilient, maintainable, and versatile. In this example, the state pattern also helped to avoid using if-else or switch-case conditional logic.
- The State Pattern is quite similar to the Strategy Pattern. If you'd like to learn more, you can look up the Strategy Pattern in Java.
State Design Pattern - UML Diagram & Implementation
The UML class diagram for the implementation of the State Design Pattern is given below:
The classes, interfaces, and objects in the above UML class diagram are as follows:
1. Context
- The Context class includes the object whose behavior changes according to its internal state.
- It stores a reference to the current state object, which represents the Context's current state.
- The Context serves as an interface for clients to interact with and usually delegate state-specific actions to the current state object.
2. State Interface/Base Class
- The State interface or base class provides a standard interface for all concrete state classes.
- This interface typically includes methods that represent the Context's state-specific behavior.
- It enables the Context to interact with state items without knowing their specific kinds.
3. Concrete States
- Concrete state classes use the State interface or extend the basic class.
- Each concrete state class represents the behavior associated with a certain state of the Context.
- These classes specify how the Context behaves in its distinct states.
Communication between the components
Communication between components in the State design pattern often involves the following steps:
Step 1: Client Interaction
- The client interacts with the Context object, either directly or indirectly, by invoking its methods.
Step 2: Behavior Delegation
- When a client initiates an action or requestsbehavior from the Context, the Context assigns responsibilitiesto the current State object.
Step 3: State-Specific Behavior Execution
- The current State object receives the delegated request and performs the behavior associated with its specific state.
Step 4: Possible State Transition.
- Depending on the logic implemented within the State object or controlled by the Context, a state transition could occur.
Step 5: Update the Current State
- If a state transition happens, the Context adjusts its reference to the new State object to reflect the change in the internal state.
Step 6: Continued interaction.
- The client continues to interact with the Context as needed, and the process is repeated, with behavior delegated to the appropriate State object based on the Context's current state.
Real-World Example of State Design Pattern
This diagram illustrates the State Design Pattern using an analogy of a vending machine. Here's a breakdown of the components:
VendingMachine Class
- Acts as the context that holds a reference to the current state.
- It triggers the behavior based on its current state, like accepting coins, dispensing products, or returning change.
- It provides methods for interacting with the machine, such as inserting coins, selecting products, and dispensing items.
- It delegates these actions to the current state.
State Interface
- Defines the common interface for all states of the vending machine.
- It ensures that each state (e.g., waiting for coins, dispensing products) can respond to actions in a standardized way.
- It specifies the behavior that the vending machine can exhibit, such as accepting coins or returning change.
ConcreteStateA and ConcreteStateB Classes
- These are specific implementations of the State interface, such as "HasCoinState" or "OutOfStockState."
- They encapsulate the logic specific to each state, like accepting coins or refusing to dispense a product when out of stock.
- The vending machine transitions between these states based on user interactions.
State Transitions
- The vending machine can move between states, such as transitioning from accepting coins to dispensing a product.
- Each state is responsible for handling transitions based on input, such as switching to a new state after an item is dispensed.
- The Vending Machine updates its current state as the internal conditions change.
Key Points
- The State Design Pattern is useful because it allows the Vending Machine to alter its behavior without changing its class.
- It separates state-specific logic into different classes, improving the maintainability and flexibility of the system.
- It makes it easy to add or modify states without altering the vending machine or other state classes.
- It's ideal for systems where an object's behavior needs to change dynamically based on its state, such as vending machines, game characters, or traffic lights.
Example
using System;
public interface IState
{
void InsertCoin(VendingMachine machine);
void SelectProduct(VendingMachine machine);
void DispenseItem(VendingMachine machine);
}
public class HasCoinState : IState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Coin already inserted.");
}
public void SelectProduct(VendingMachine machine)
{
Console.WriteLine("Product selected.");
machine.SetState(new DispensingState());
}
public void DispenseItem(VendingMachine machine)
{
Console.WriteLine("Select a product first.");
}
}
public class OutOfStockState : IState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Machine is out of stock. Coin rejected.");
}
public void SelectProduct(VendingMachine machine)
{
Console.WriteLine("Machine is out of stock.");
}
public void DispenseItem(VendingMachine machine)
{
Console.WriteLine("No product to dispense. Machine is out of stock.");
}
}
public class DispensingState : IState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Please wait, we're already dispensing your item.");
}
public void SelectProduct(VendingMachine machine)
{
Console.WriteLine("Item already selected, dispensing in progress.");
}
public void DispenseItem(VendingMachine machine)
{
Console.WriteLine("Dispensing item...");
machine.SetState(new HasCoinState()); // Reset to HasCoinState for next transaction
}
}
public class VendingMachine
{
private IState _currentState;
public VendingMachine()
{
_currentState = new OutOfStockState();
}
public void SetState(IState state)
{
_currentState = state;
}
public void InsertCoin()
{
_currentState.InsertCoin(this);
}
public void SelectProduct()
{
_currentState.SelectProduct(this);
}
public void DispenseItem()
{
_currentState.DispenseItem(this);
}
public void Refill()
{
Console.WriteLine("Machine refilled.");
SetState(new HasCoinState());
}
}
public class Program
{
public static void Main(string[] args)
{
VendingMachine machine = new VendingMachine();
machine.InsertCoin();
machine.SelectProduct();
machine.DispenseItem();
machine.Refill();
machine.InsertCoin();
machine.SelectProduct();
machine.DispenseItem();
}
}
interface State {
void insertCoin(VendingMachine machine);
void selectProduct(VendingMachine machine);
void dispenseItem(VendingMachine machine);
}
class HasCoinState implements State {
public void insertCoin(VendingMachine machine) {
System.out.println("Coin already inserted.");
}
public void selectProduct(VendingMachine machine) {
System.out.println("Product selected.");
machine.setState(new DispensingState());
}
public void dispenseItem(VendingMachine machine) {
System.out.println("Select a product first.");
}
}
class OutOfStockState implements State {
public void insertCoin(VendingMachine machine) {
System.out.println("Machine is out of stock. Coin rejected.");
}
public void selectProduct(VendingMachine machine) {
System.out.println("Machine is out of stock.");
}
public void dispenseItem(VendingMachine machine) {
System.out.println("No product to dispense. Machine is out of stock.");
}
}
class DispensingState implements State {
public void insertCoin(VendingMachine machine) {
System.out.println("Please wait, we're already dispensing your item.");
}
public void selectProduct(VendingMachine machine) {
System.out.println("Item already selected, dispensing in progress.");
}
public void dispenseItem(VendingMachine machine) {
System.out.println("Dispensing item...");
machine.setState(new HasCoinState()); // Reset to HasCoinState for next transaction
}
}
class VendingMachine {
private State currentState;
public VendingMachine() {
currentState = new OutOfStockState();
}
public void setState(State state) {
this.currentState = state;
}
public void insertCoin() {
currentState.insertCoin(this);
}
public void selectProduct() {
currentState.selectProduct(this);
}
public void dispenseItem() {
currentState.dispenseItem(this);
}
public void refill() {
System.out.println("Machine refilled.");
setState(new HasCoinState());
}
}
public class Main {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
machine.refill();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
}
}
class State:
def insert_coin(self, machine):
pass
def select_product(self, machine):
pass
def dispense_item(self, machine):
pass
class HasCoinState(State):
def insert_coin(self, machine):
print("Coin already inserted.")
def select_product(self, machine):
print("Product selected.")
machine.set_state(DispensingState())
def dispense_item(self, machine):
print("Select a product first.")
class OutOfStockState(State):
def insert_coin(self, machine):
print("Machine is out of stock. Coin rejected.")
def select_product(self, machine):
print("Machine is out of stock.")
def dispense_item(self, machine):
print("No product to dispense. Machine is out of stock.")
class DispensingState(State):
def insert_coin(self, machine):
print("Please wait, we're already dispensing your item.")
def select_product(self, machine):
print("Item already selected, dispensing in progress.")
def dispense_item(self, machine):
print("Dispensing item...")
machine.set_state(HasCoinState()) # Reset to HasCoinState for next transaction
class VendingMachine:
def __init__(self):
self.current_state = OutOfStockState()
def set_state(self, state):
self.current_state = state
def insert_coin(self):
self.current_state.insert_coin(self)
def select_product(self):
self.current_state.select_product(self)
def dispense_item(self):
self.current_state.dispense_item(self)
def refill(self):
print("Machine refilled.")
self.set_state(HasCoinState())
# Testing the Vending Machine
machine = VendingMachine()
machine.insert_coin()
machine.select_product()
machine.dispense_item()
machine.refill()
machine.insert_coin()
machine.select_product()
machine.dispense_item()
interface State {
insertCoin(machine: VendingMachine): void;
selectProduct(machine: VendingMachine): void;
dispenseItem(machine: VendingMachine): void;
}
class HasCoinState implements State {
insertCoin(machine: VendingMachine) {
console.log("Coin already inserted.");
}
selectProduct(machine: VendingMachine) {
console.log("Product selected.");
machine.setState(new DispensingState());
}
dispenseItem(machine: VendingMachine) {
console.log("Select a product first.");
}
}
class OutOfStockState implements State {
insertCoin(machine: VendingMachine) {
console.log("Machine is out of stock. Coin rejected.");
}
selectProduct(machine: VendingMachine) {
console.log("Machine is out of stock.");
}
dispenseItem(machine: VendingMachine) {
console.log("No product to dispense. Machine is out of stock.");
}
}
class DispensingState implements State {
insertCoin(machine: VendingMachine) {
console.log("Please wait, we're already dispensing your item.");
}
selectProduct(machine: VendingMachine) {
console.log("Item already selected, dispensing in progress.");
}
dispenseItem(machine: VendingMachine) {
console.log("Dispensing item...");
machine.setState(new HasCoinState()); // Reset to HasCoinState for next transaction
}
}
class VendingMachine {
private currentState: State;
constructor() {
this.currentState = new OutOfStockState();
}
setState(state: State) {
this.currentState = state;
}
insertCoin() {
this.currentState.insertCoin(this);
}
selectProduct() {
this.currentState.selectProduct(this);
}
dispenseItem() {
this.currentState.dispenseItem(this);
}
refill() {
console.log("Machine refilled.");
this.setState(new HasCoinState());
}
}
// Testing the Vending Machine
let machine = new VendingMachine();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
machine.refill();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
class State {
insertCoin(machine) {}
selectProduct(machine) {}
dispenseItem(machine) {}
}
class HasCoinState extends State {
insertCoin(machine) {
console.log("Coin already inserted.");
}
selectProduct(machine) {
console.log("Product selected.");
machine.setState(new DispensingState());
}
dispenseItem(machine) {
console.log("Select a product first.");
}
}
class OutOfStockState extends State {
insertCoin(machine) {
console.log("Machine is out of stock. Coin rejected.");
}
selectProduct(machine) {
console.log("Machine is out of stock.");
}
dispenseItem(machine) {
console.log("No product to dispense. Machine is out of stock.");
}
}
class DispensingState extends State {
insertCoin(machine) {
console.log("Please wait, we're already dispensing your item.");
}
selectProduct(machine) {
console.log("Item already selected, dispensing in progress.");
}
dispenseItem(machine) {
console.log("Dispensing item...");
machine.setState(new HasCoinState()); // Reset to HasCoinState for next transaction
}
}
class VendingMachine {
constructor() {
this.currentState = new OutOfStockState();
}
setState(state) {
this.currentState = state;
}
insertCoin() {
this.currentState.insertCoin(this);
}
selectProduct() {
this.currentState.selectProduct(this);
}
dispenseItem() {
this.currentState.dispenseItem(this);
}
refill() {
console.log("Machine refilled.");
this.setState(new HasCoinState());
}
}
// Testing the Vending Machine
let machine = new VendingMachine();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
machine.refill();
machine.insertCoin();
machine.selectProduct();
machine.dispenseItem();
Explanation
- This code demonstrates the State pattern. It is where the vending machine transitions between different states, such as HasCoinState, OutOfStockState, and DispensingState.
- Each state defines its behavior when a user interacts with the machine.
- The VendingMachine class handles these state transitions, starting as out of stock and then being refilled to handle coin insertion, product selection, and dispensing of items.
When to use the State Design Pattern
- Object behavior changes with state: It is helpful when an object’s behavior changes based on its current state, and you want to manage this cleanly.
- Avoiding complex conditional logic:It is ideal to use many conditionals or switch statements to handle state transitions, as it simplifies the code.
- Encapsulating state logic: It is useful when you want to encapsulate different state-specific behaviors into separate classes, making the system easier to extend or modify.
When not to use the State Design Pattern
The State Design Pattern may not be ideal in the following cases:- Simple state management: If your object only has a few states or transitions, it is unnecessary to use the pattern, as it adds complexity.
- No frequent state changes: If the object’s state doesn’t change frequently, it is better to avoid the overhead of implementing this pattern.
- Minimal behavior variation: It is not suitable when the different states don’t significantly affect the object's behavior, making the pattern redundant.
Relationship with Other Patterns
Mediator Pattern
- It simplifies communication between objects by acting as an intermediary, while the State Pattern allows an object to change its behavior based on its state.
- The Mediator handles communication, while the State Pattern focuses on state-driven behavior changes.
Adapter Pattern
- It adapts one interface to another to make them compatible.
- In contrast, the State Pattern dynamically changes an object’s behavior based on its internal state rather than adapting interfaces.
Bridge Pattern
- It separates abstraction from implementation so that both can evolve independently.
- The State Pattern changes the behavior of an object based on its state, while the Bridge Pattern focuses on separating abstraction and implementation.
Abstract Factory Pattern
- It creates families of related objects.
- The State Pattern doesn’t deal with object creation but allows objects to change behavior at runtime based on their state, unlike the Abstract Factory, which focuses on object creation.
Decorator Pattern
- It is used to add responsibilities to objects dynamically.
- The State Pattern, however, changes the behavior of an object depending on its state, while the Decorator Pattern focuses on adding new functionalities.