The State Design Pattern allows an object to change its behavior when its internal state changes. This pattern is particularly useful when an object must behave differently depending on its state, without requiring modifications to the code that interacts with it.
When to Use the State Pattern?
The State Pattern is ideal when:
- An object has multiple states, each with different behaviors.
- Avoiding complex conditional statements (
if-else
orswitch-case
) to handle state transitions. - You want to encapsulate state-specific behavior in separate classes for better maintainability.
Implementing the State Pattern in Java
Let's look at a simple implementation using a vending machine that can be in different states: NoCredit
, WithCredit
, and DispensingBeverage
.
1. State Interface
The interface defines behaviors that vary based on the machine's state.
public interface State {
void insertCredit();
void ejectCredit();
void dispenseBeverage();
}
2. Context Class (Vending Machine)
This class maintains the current state and dynamically updates it as actions are performed.
public class VendingMachine {
private State currentState;
private final State noCreditState = new NoCredit(this);
private final State withCreditState = new WithCredit(this);
private final State dispensingState = new DispensingBeverage(this);
public VendingMachine() {
this.currentState = noCreditState;
}
public void setState(State newState) {
this.currentState = newState;
}
public State getNoCreditState() { return noCreditState; }
public State getWithCreditState() { return withCreditState; }
public State getDispensingState() { return dispensingState; }
public void insertCredit() { currentState.insertCredit(); }
public void ejectCredit() { currentState.ejectCredit(); }
public void dispenseBeverage() { currentState.dispenseBeverage(); }
}
3. State Implementations
Each state class implements the State
interface and modifies the machine's state when appropriate.
// State: No Credit
class NoCredit implements State {
private VendingMachine machine;
public NoCredit(VendingMachine machine) { this.machine = machine; }
@Override
public void insertCredit() {
System.out.println("Credit inserted. You can now buy a beverage.");
machine.setState(machine.getWithCreditState());
}
@Override
public void ejectCredit() { System.out.println("No credit to eject."); }
@Override
public void dispenseBeverage() { System.out.println("No credit. Cannot dispense beverage."); }
}
// State: With Credit
class WithCredit implements State {
private VendingMachine machine;
public WithCredit(VendingMachine machine) { this.machine = machine; }
@Override
public void insertCredit() { System.out.println("Credit already inserted."); }
@Override
public void ejectCredit() {
System.out.println("Credit ejected. The machine now has no credit.");
machine.setState(machine.getNoCreditState());
}
@Override
public void dispenseBeverage() {
System.out.println("Beverage dispensed. Enjoy!");
machine.setState(machine.getDispensingState());
}
}
// State: Dispensing Beverage
class DispensingBeverage implements State {
private VendingMachine machine;
public DispensingBeverage(VendingMachine machine) { this.machine = machine; }
@Override
public void insertCredit() { System.out.println("The machine is dispensing. Please wait."); }
@Override
public void ejectCredit() { System.out.println("Cannot eject credit while dispensing."); }
@Override
public void dispenseBeverage() {
System.out.println("Beverage already dispensed. Waiting for next transaction.");
machine.setState(machine.getNoCreditState());
}
}
4. Testing the Vending Machine
With the improved design, the state transitions happen automatically within the state classes.
public class VendingMachineTest {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
machine.insertCredit(); // Insert credit → Changes to "With Credit"
machine.dispenseBeverage(); // Dispense beverage → Changes to "Dispensing"
machine.dispenseBeverage(); // Complete transaction → Returns to "No Credit"
machine.ejectCredit(); // No credit to eject
}
}
Expected Output
Credit inserted. You can now buy a beverage.
Beverage dispensed. Enjoy!
Beverage already dispensed. Waiting for next transaction.
No credit to eject.
Advantages of the State Pattern
✅ Encapsulation of Behavior – Each state has its own behavior, making the code easier to maintain.
✅ Eliminates Complex Conditionals – Avoids large if-else
or switch
statements in the context class.
✅ Easier to Add New States – You can introduce new states without modifying the existing ones.
✅ Improved Code Readability – The logic for each state is clearly separated, making the code more organized.
Disadvantages of the State Pattern
❌ Increased Number of Classes – Requires a new class for each state, which can lead to more complex structures.
❌ Overhead in Simple Scenarios – If an object has only a few states, a simple switch
statement may be more efficient.
Conclusion
The State Pattern is a powerful tool when dealing with objects that change behavior based on their state. By encapsulating state-specific behavior into separate classes, we create a cleaner, more maintainable, and scalable design. However, if the number of states is minimal, the overhead of additional classes may not be worth it.
Top comments (0)