The State Pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
This is a proper implementation of the State Pattern using separate state classes. Each state encapsulates its own behavior, and the context (GumballMachine) delegates actions to the current state object. This demonstrates the full benefits of the State Pattern.
- Context: The
GumballMachineclass that maintains a reference to the current state object - State Interface: The
Stateinterface that defines the common behavior for all states - Concrete States: Separate classes for each state (NoQuarterState, HasQuarterState, SoldState, SoldOutState, WinnerState)
- State Transitions: Each state handles its own transitions by calling
setState()on the context - Encapsulated Behavior: Each state class contains all the behavior specific to that state
This implementation shows a gumball machine with five states:
- SoldOutState - No gumballs available
- NoQuarterState - Ready to accept a quarter
- HasQuarterState - Quarter inserted, ready to dispense
- SoldState - Currently dispensing a gumball
- WinnerState - Lucky winner! Dispensing two gumballs
The machine handles four main actions through the State interface:
insertQuarter()- Insert a quarter into the machineejectQuarter()- Request a refund of the quarterturnCrank()- Turn the crank to get a gumballdispense()- Dispense the appropriate number of gumballs
This implementation adds a WinnerState to demonstrate the extensibility of the State Pattern. When a quarter is inserted and the crank is turned:
- There's a 10% chance (1 in 10) the machine enters the WinnerState
- Winners receive two gumballs instead of one
- The WinnerState is only entered if there are at least 2 gumballs remaining
┌──────────────┐
│ SOLD_OUT │
└──────┬───────┘
│ refill()
│ [count > 0]
▼
┌──────────────┐
┌─────────▶│ NO_QUARTER │◀─────────┐
│ └──────┬───────┘ │
│ │ │
│ │ insertQuarter() │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ HAS_QUARTER │ │
│ └──┬────────┬──┘ │
│ │ │ │
│ ejectQuarter() turnCrank() │
│ │ │ │
│ │ │ │
│ │ [90%] │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ SOLD │ │
│ │ └─────┬────┘ │
│ │ │ │
│ │ dispense() │
│ │ │ │
│ │ [count > 0] │
│ └─────────┴─────────────┘
│ │
│ [count == 0]
│ │
│ ▼
│ ┌──────────────┐
└────────────────│ SOLD_OUT │
└──────────────┘
[10% AND count > 1]
│
▼
┌──────────────┐
│ WINNER │
└──────┬───────┘
│
dispense()
(2 gumballs released)
│
[count > 0]
│
┌────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ NO_QUARTER │ │ 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 (90% of the time)
- HAS_QUARTER → WINNER: When crank is turned (10% chance, if count > 1)
- SOLD → NO_QUARTER: After dispensing, if gumballs remain
- SOLD → SOLD_OUT: After dispensing the last gumball
- WINNER → NO_QUARTER: After dispensing 2 gumballs, if gumballs remain
- WINNER → SOLD_OUT: After dispensing, if no gumballs remain
- State.java - Interface defining the contract for all states
- GumballMachine.java - The context that maintains state and delegates behavior
- NoQuarterState.java - State when machine is ready to accept a quarter
- HasQuarterState.java - State when quarter is inserted (includes winner logic)
- SoldState.java - State when dispensing a single gumball
- SoldOutState.java - State when machine has no gumballs
- WinnerState.java - State when dispensing two gumballs
- GumballMachineTestDrive.java - Test driver demonstrating the state machine
From the parent directory of StatePattern:
javac -d out StatePattern/FinalVersion/*.javajava -cp out StatePattern.FinalVersion.GumballMachineTestDriveThe test drive will demonstrate various scenarios:
- Successfully purchasing gumballs
- Random winner occurrences (10% probability)
- Machine state transitions
- Automatic state management through delegation
Note: Since the winner state has a random component (10% chance), the output will vary between runs. You may see either "YOU'RE A WINNER!" messages with two gumballs dispensed, or normal single-gumball purchases.
This implementation properly uses the State Pattern with separate state classes, providing significant benefits:
- Easy to Extend: Adding the WinnerState required only creating a new state class - no modifications to existing states
- Follows Open/Closed Principle: Open for extension (new states), closed for modification (existing code unchanged)
- Encapsulated State Logic: Each state class contains all behavior specific to that state
- No Conditional Complexity: The context simply delegates to the current state - no if-else chains
- Localized Changes: State-specific behavior changes only affect that state's class
To add the WinnerState to this implementation, only these steps were needed:
- Create
WinnerState.javaimplementing theStateinterface - Add a
winnerStatefield and getter inGumballMachine - Initialize the
winnerStatein theGumballMachineconstructor - Modify
HasQuarterState.turnCrank()to randomly transition to WinnerState - Update
toString()to display the winner state message
No other state classes were modified - this demonstrates the power of the State Pattern's encapsulation.
- Eliminates conditional complexity: Removes large if-else or switch statements
- Encapsulates state-specific behavior: Each state's behavior is localized in its own class
- Makes state transitions explicit: Clear transitions through
setState()calls - Easy to add new states: Simply add new state classes without modifying existing code
- Open/Closed Principle: Open for extension, closed for modification
- Single Responsibility: Each state class has one reason to change
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
- You want to make state transitions explicit and easy to understand
- You anticipate adding new states in the future
The FirstTry version uses integer constants and if-else statements, making it difficult to extend. This FinalVersion demonstrates the proper State Pattern implementation where:
- Each state is a separate class implementing the
Stateinterface - The context delegates behavior to the current state object
- Adding new states (like WinnerState) requires minimal changes
- No complex conditional logic in the context class
While this implementation demonstrates the State Pattern effectively, there are several enhancements that could improve the design:
Problem: Multiple state classes (SoldState, WinnerState, SoldOutState) have identical implementations for insertQuarter(), ejectQuarter(), and turnCrank():
// Repeated in SoldState and WinnerState
public void insertQuarter() {
System.out.println("Please wait, we're already giving you a gumball");
}
public void ejectQuarter() {
System.out.println("Sorry, you already turned the crank");
}
public void turnCrank() {
System.out.println("Turning twice doesn't get you another gumball");
}Solution: Create an abstract base class with default implementations:
public abstract class AbstractState implements State {
protected GumballMachine gumballMachine;
public AbstractState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
// Default implementations - subclasses override as needed
public void insertQuarter() {
System.out.println("Invalid action in current state");
}
public void ejectQuarter() {
System.out.println("Invalid action in current state");
}
public void turnCrank() {
System.out.println("Invalid action in current state");
}
public void dispense() {
System.out.println("No gumball dispensed");
}
}Advantages:
- Eliminates duplicate code across state classes
- Provides sensible defaults for invalid actions
- State classes only override methods they need to customize
- Easier maintenance - change default behavior in one place
Disadvantages:
- Less explicit - harder to see at a glance which actions each state handles
- Inheritance introduces coupling between abstract class and concrete states
- May hide bugs if a state forgets to override a method it should implement
Problem: In GumballMachine.java:37-40, the turnCrank() method automatically calls dispense():
public void turnCrank(){
state.turnCrank();
state.dispense(); // Always called, even if turnCrank() failed
}This means dispense() is called regardless of whether turnCrank() was valid for the current state. For example, in NoQuarterState, it prints "You need to pay first" even though no crank turn occurred.
Solution Option A - Return Success Status:
// State interface
public interface State {
boolean turnCrank(); // Returns true if successful
void dispense();
// ... other methods
}
// GumballMachine
public void turnCrank(){
if (state.turnCrank()) {
state.dispense();
}
}Solution Option B - Integrate dispense() into turnCrank():
// Remove dispense() from the public interface
// Each state handles dispensing within turnCrank() if needed
public void turnCrank(){
state.turnCrank(); // State handles dispense internally
}Advantages:
- Option A: Explicit success/failure signaling, maintains separation of concerns
- Option B: Simpler interface, enforces that dispense follows valid crank turns
- Both prevent invalid dispense messages
Disadvantages:
- Option A: Adds boolean return type, states must remember to return correct value
- Option B: Couples turn and dispense actions together, less granular control
Current Approach: States control their own transitions by calling gumballMachine.setState().
Alternative Approach: Move transition logic to the GumballMachine context.
// In NoQuarterState
public void insertQuarter() {
System.out.println("You inserted a quarter");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}Advantages:
- Each state is self-contained and knows its own exit conditions
- Easy to see state behavior and transitions in one place
- States are independent - adding new states doesn't require changing context
- Follows encapsulation - state logic stays with the state
Disadvantages:
- States must know about other states (tighter coupling)
- Transition logic is scattered across multiple state classes
- Harder to visualize the complete state machine
- States need reference to context to call
setState()
// State interface - methods return next state or null
public interface State {
State insertQuarter();
State ejectQuarter();
State turnCrank();
void dispense();
}
// In GumballMachine
public void insertQuarter() {
State nextState = state.insertQuarter();
if (nextState != null) {
state = nextState;
}
}
// In NoQuarterState
public State insertQuarter() {
System.out.println("You inserted a quarter");
return gumballMachine.getHasQuarterState();
}Advantages:
- Centralized transition control - easier to audit all transitions
- State classes don't need setState() access (looser coupling)
- Can add transition validation/logging in one place
- State machine flow is visible in the context class
Disadvantages:
- States must return their successor state (may not always be clear)
- Context must understand when to actually transition
- More responsibility in the context class
- Harder to make state transitions conditional on context state
Recommendation: The current approach (states control transitions) is generally preferred for the State Pattern because it keeps state logic encapsulated and makes states self-contained.
Problem: Every GumballMachine instance creates its own set of 5 state objects. If you instantiate many gumball machines, you're creating many identical state objects.
Current Approach:
public GumballMachine(int numberGumballs){
soldOutState = new SoldOutState(this); // New objects
noQuarterState = new NoQuarterState(this); // for each
hasQuarterState = new HasQuarterState(this); // machine
soldState = new SoldState(this); // instance
winnerState = new WinnerState(this);
// ...
}Solution - Static State Instances:
// Static state instances shared across all machines
private static final State SOLD_OUT_STATE = new SoldOutState();
private static final State NO_QUARTER_STATE = new NoQuarterState();
private static final State HAS_QUARTER_STATE = new HasQuarterState();
private static final State SOLD_STATE = new SoldState();
private static final State WINNER_STATE = new WinnerState();
// State methods take context as parameter
public interface State {
void insertQuarter(GumballMachine context);
void ejectQuarter(GumballMachine context);
void turnCrank(GumballMachine context);
void dispense(GumballMachine context);
}
// In NoQuarterState
public void insertQuarter(GumballMachine context) {
System.out.println("You inserted a quarter");
context.setState(NO_QUARTER_STATE);
}
// In GumballMachine
public void insertQuarter() {
state.insertQuarter(this);
}Advantages:
- Memory efficiency: One set of state objects for all machines instead of N sets
- Performance: No object creation overhead in constructor
- Immutable states are thread-safe by default
- Follows Flyweight pattern for shared, stateless objects
Disadvantages:
- States cannot hold per-machine state (e.g.,
RandomWinnerinHasQuarterState) - More complex method signatures - must pass context to every method
- If states need instance-specific data, this approach doesn't work
- Breaking change: HasQuarterState has
Random randomWinnerinstance variable
Note: This approach won't work as-is for this implementation because HasQuarterState has an instance variable (Random randomWinner). You would need to either:
- Move the random number generation to the context
- Pass a random number generator to the state methods
- Accept that HasQuarterState must be per-instance
Hybrid Approach:
// Share stateless states
private static final State SOLD_OUT_STATE = new SoldOutState();
private static final State NO_QUARTER_STATE = new NoQuarterState();
private static final State SOLD_STATE = new SoldState();
private static final State WINNER_STATE = new WinnerState();
// Keep stateful state as instance variable
private final State hasQuarterState;
public GumballMachine(int numberGumballs) {
hasQuarterState = new HasQuarterState(this); // Only one instance needed
// ...
}| Improvement | Complexity | Benefit | When to Use |
|---|---|---|---|
| Abstract base class | Low | Reduces duplication | When states share common default behavior |
| Fix dispense() call | Low-Medium | Prevents invalid messages | Always recommended |
| Context-controlled transitions | Medium | Centralized flow control | When you need audit/validation of transitions |
| Static state instances | Medium-High | Memory/performance savings | When you have many context instances and stateless states |
Recommendation: Start with fixing the dispense() issue (improvement #2) as it's a clear bug. Consider the abstract base class (improvement #1) if code duplication becomes a maintenance burden. The other improvements are optimizations that may not be necessary unless you have specific performance or architectural requirements.