๐ฌ State Pattern: Intelligent Gumball Vendor
๐ Overview
The State Pattern allows an object to alter its behavior when its internal state changes. It encapsulates state-specific behaviors into separate classes, removing massive if-else or switch statements and allowing the object to "change its class" at runtime as it transitions through a lifecycle.
Core Concepts
- Finite State Machine (FSM): The pattern provides a structured way to implement complex state-transition diagrams.
- Context Delegation: The main object (e.g., Gumball Machine) delegates all actions to the current state object.
- Polymorphic Transitions: Each state class handles its own logic and decides when to trigger a transition to the next state.
๐ญ The Engineering Story & Problem
๐ก The Villain (The Problem)
You're building the software for a classic Gumball Machine. It has four states: No Quarter, Has Quarter, Gumball Sold, and Out of Gumballs.
The "Nested If-Else Monster" version looks like this:
class GumballMachine:
def turn_crank(self):
if self.state == HAS_QUARTER:
if self.count > 0:
self.dispense()
self.state = SOLD
else:
self.state = SOLD_OUT
elif self.state == NO_QUARTER:
print("Please insert a quarter first!")
# ... repeat for every action ...
if/elif blocks. the code becomes a tangled web of conditional logic that is impossible to test or extend.
๐ฆธ The Hero (The Solution)
The State Pattern introduces "State Objects."
Instead of the machine checking its state, we create classes like NoQuarterState, HasQuarterState, and SoldState.
The GumballMachine (Context) just holds a reference to the current_state. When you turn the crank, it just says: self.current_state.turn_crank().
- If it's in HasQuarterState, the crank works and it dispenses a gumball.
- If it's in NoQuarterState, it tells you to insert money.
Each state is a small, focused class. To add a "Winner" feature, you just add one new WinnerState class and update the transition in HasQuarterState. You don't have to touch the GumballMachine or SoldOutState at all.
๐ Requirements & Constraints
- (Functional): implement the lifecycle of a gumball machine (Insert -> Turn -> Dispense).
- (Technical): Transitions must be handled by state objects.
- (Technical): The machine must handle the "Sold Out" state gracefully.
๐๏ธ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class State {
<<interface>>
+insert_quarter()
+eject_quarter()
+turn_crank()
+dispense()
}
class NoQuarterState {
+insert_quarter()
...
}
class HasQuarterState {
+turn_crank()
...
}
class GumballMachine {
-state: State
-count: int
+set_state(state)
+release_ball()
}
State <|.. NoQuarterState
State <|.. HasQuarterState
GumballMachine o-- State : delegates to
Runtime Context (Sequence)
sequenceDiagram
participant User
participant Machine
participant State as HasQuarterState
User->>Machine: turn_crank()
Machine->>State: turn_crank()
State->>Machine: release_ball()
State->>Machine: set_state(NoQuarterState)
Machine-->>User: Gumball rolls out!
๐ป Implementation & Code
๐ง SOLID Principles Applied
- Single Responsibility: Each state class handles logic for exactly one machine state.
- Open/Closed: You can add a
WinnerStatewithout changing the existingSoldOutStateorNoQuarterState.
๐ The Code
The Villain's Code (Without Pattern)
The Hero's Code (With Pattern)
from abc import ABC, abstractmethod
class State(ABC):
@abstractmethod
def insert_quarter(self) -> "State":
pass
@abstractmethod
def eject_quarter(self) -> "State":
pass
@abstractmethod
def turn_crank(self, machine: "GumballMachine") -> "State":
pass
class NoQuarterState(State):
def insert_quarter(self) -> State:
return HasQuarterState()
def eject_quarter(self) -> State:
return self
def turn_crank(self, machine: "GumballMachine") -> State:
print("You need to pay first")
return self
class HasQuarterState(State):
def insert_quarter(self) -> State:
return self
def eject_quarter(self) -> State:
return NoQuarterState()
def turn_crank(self, machine: "GumballMachine") -> State:
machine.state = SoldState()
print("Quarter is their now turning the crank")
return machine.state.turn_crank(machine)
class SoldState(State):
def insert_quarter(self) -> State:
return self
def eject_quarter(self) -> State:
return self
def turn_crank(self, machine: "GumballMachine") -> State:
count = machine.count
if count > 0:
print("Has gumballs")
count = count - 1
machine.count = count
machine.state = NoQuarterState()
return NoQuarterState()
else:
machine.state = OutOfGumballsState()
return OutOfGumballsState()
class OutOfGumballsState(State):
def insert_quarter(self) -> State:
print("No Gumballs Left in the Machine")
return self
def eject_quarter(self) -> State:
print("We refill the Gumball soon in the Machine. Thanks for visiting")
return self
def turn_crank(self, machine: "GumballMachine") -> State:
print("No Gumballs Left in the Machine so can not give you anything")
return self
class GumballMachine:
def __init__(self, count: int) -> None:
self.count : int = count
self.state : State = NoQuarterState()
def insert_quarter(self) -> None:
self.state = self.state.insert_quarter()
def eject_quarter(self) -> None:
self.state = self.state.eject_quarter()
def turn_crank(self) -> None:
self.state = self.state.turn_crank(self)
my_machine = GumballMachine(count=3)
my_machine.turn_crank()
my_machine.insert_quarter()
my_machine.turn_crank()
my_machine.insert_quarter()
my_machine.turn_crank()
my_machine.insert_quarter()
my_machine.turn_crank()
my_machine.insert_quarter()
my_machine.turn_crank()
my_machine.insert_quarter()
my_machine.turn_crank()
โ๏ธ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Encapsulation: All state logic is in one place. | Class Explosion: One class for every single state. |
Readability: Replaces complex if-else with simple calls. |
Complexity: Harder to follow the overall flow "at a glance." |
| FSM Alignment: Perfectly maps to Finite State Machine diagrams. | Transitions: States must often know about each other to trigger changes. |
๐งช Testing Strategy
- Unit Test States: Test
HasQuarterState.turn_crank()in isolation by mocking theGumballMachine. - Workflow Test: Insert quarter -> turn crank -> verify inventory decreased and state is now
NoQuarter. - Boundary Test: Empty the machine and verify it stays in
SoldOutStateregardless of input.
๐ค Interview Toolkit
- Interview Signal: mastery of state-driven logic and polymorphism.
- When to Use:
- "Build a vending machine or ATM..."
- "Handle complex UI navigation (Wizards)..."
- "Implement a network protocol (TCP/IP handshake)..."
- Scalability Probe: "How to handle persistence?" (Answer: Store the state name in the DB. When loading, use a Factory to re-create the correct State object.)
- Design Alternatives:
- Strategy: If the behavior doesn't change based on phases but just based on configuration.