The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
The State Pattern encapsulates state-based behavior and delegates behavior to the current state. This example demonstrates a gumball machine with different states and transitions.
- Context: The
GumballMachineclass that maintains a reference to the current state - States: Different states the machine can be in (SOLD_OUT, NO_QUARTER, HAS_QUARTER, SOLD)
- State Transitions: Actions (insertQuarter, ejectQuarter, turnCrank, dispense) cause transitions between states
- Encapsulated Behavior: Each state handles actions differently based on the current state
This implementation shows a gumball machine with four states:
- SOLD_OUT - No gumballs available
- NO_QUARTER - Ready to accept a quarter
- HAS_QUARTER - Quarter inserted, ready to dispense
- SOLD - Currently dispensing a gumball
The machine handles four main actions:
insertQuarter()- Insert a quarter into the machineejectQuarter()- Request a refund of the quarterturnCrank()- Turn the crank to get a gumballdispense()- Internal method to dispense a gumball
┌──────────────┐
│ SOLD_OUT │
└──────┬───────┘
│ refill()
│ [count > 0]
▼
┌──────────────┐
┌─────────▶│ NO_QUARTER │◀─────────┐
│ └──────┬───────┘ │
│ │ │
│ │ insertQuarter() │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ HAS_QUARTER │ │
│ └──┬────────┬──┘ │
│ │ │ │
│ ejectQuarter() turnCrank() │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ SOLD │ │
│ │ └─────┬────┘ │
│ │ │ │
│ │ dispense() │
│ │ │ │
│ │ [count > 0] │
└─────────────┴─────────┴─────────────┘
│
[count == 0]
│
▼
┌──────────────┐
│ SOLD_OUT │
└──────────────┘
- SOLD_OUT → NO_QUARTER: When machine is refilled with gumballs
- NO_QUARTER → HAS_QUARTER: When a quarter is inserted
- HAS_QUARTER → NO_QUARTER: When quarter is ejected
- HAS_QUARTER → SOLD: When crank is turned
- SOLD → NO_QUARTER: After dispensing, if gumballs remain
- SOLD → SOLD_OUT: After dispensing the last gumball
- GumballMachine.java - The main state machine implementation
- GumballMachineTestDrive.java - Test driver demonstrating various state transitions
From the parent directory of StatePattern:
javac -d out StatePattern/FirstTry/*.javajava -cp out StatePattern.FirstTry.GumballMachineTestDriveThe test drive will demonstrate various scenarios:
- Successfully purchasing a gumball
- Inserting and ejecting a quarter
- Attempting to turn crank without a quarter
- Multiple purchase attempts
- Machine running out of gumballs
Note: This implementation uses integer constants and conditional statements (if-else) to manage states, which does not fully leverage the State Pattern's benefits. This approach has significant drawbacks:
- Difficult to Extend: Adding a new state (like a WINNER state) requires modifying multiple methods throughout the
GumballMachineclass - Violates Open/Closed Principle: You must modify existing code rather than extending it
- Scattered State Logic: State-specific behavior is spread across multiple if-else blocks instead of being encapsulated
Consider adding a WINNER state where 10% of crank turns dispense 2 gumballs instead of 1. With the current implementation, you would need to:
- Add a new constant:
final static int WINNER = 4; - Modify
turnCrank()to randomly transition to WINNER instead of SOLD - Modify
dispense()to handle the WINNER state (dispense 2 gumballs) - Update all other methods (
insertQuarter(),ejectQuarter(),toString()) to handle the WINNER case - Ensure proper state transitions from WINNER back to NO_QUARTER or SOLD_OUT
This requires changes across the entire class, making the code fragile and error-prone.
A proper State Pattern implementation would use separate state classes (NoQuarterState, HasQuarterState, SoldState, SoldOutState, WinnerState) where:
- Each state class encapsulates its own behavior
- Adding a new state only requires creating a new state class
- The context (GumballMachine) simply delegates to the current state object
- No modifications to existing state classes are needed
This implementation serves as a "before" example showing why the State Pattern with separate state classes is beneficial.
- Eliminates conditional complexity: Removes large if-else or switch statements
- Encapsulates state-specific behavior: Each state's behavior is localized
- Makes state transitions explicit: Clear transitions between states
- Easy to add new states: Simply add new state classes without modifying existing code
- Open/Closed Principle: Open for extension, closed for modification
Use the State Pattern when:
- An object's behavior depends on its state and must change at runtime
- Operations have large conditional statements that depend on the object's state
- State-specific behavior needs to be encapsulated and made interchangeable