How to Implement the State Pattern for Object Behavior Transitions in Java
The State pattern allows an object to alter its behavior at runtime by delegating operations to state-specific classes, eliminating complex conditional logic.
The State pattern is a behavioral design pattern that enables an object to change its behavior when its internal state changes, appearing as if the object changed its class. In the iluwatar/java-design-patterns repository, this pattern is demonstrated through a Mammoth that transitions between peaceful and angry moods. This guide shows you how to implement the State pattern for object behavior transitions in Java using the exact approach found in the reference implementation.
State Pattern Architecture
The implementation consists of three core components that work together to encapsulate state-specific behavior and manage transitions.
- State Interface: Defines the contract that all concrete states must implement, ensuring consistent behavior across different states.
- Concrete States: Individual classes that implement the
Stateinterface, each containing behavior specific to that state. - Context: The class that maintains a reference to the current state and delegates behavior to it, triggering transitions when conditions change.
Step-by-Step Implementation
Define the State Interface
Create an interface that declares the methods representing state-specific behavior. In state/src/main/java/com/iluwatar/state/State.java, the interface defines two methods: one for entry actions and one for the primary behavior.
public interface State {
void onEnterState(); // called when the context enters this state
void observe(); // behavior exposed to the client
}
Create Concrete State Classes
Implement the State interface for each distinct state. Each concrete state receives a reference to the context (Mammoth) in its constructor and implements state-specific behavior.
PeacefulState.java (state/src/main/java/com/iluwatar/state/PeacefulState.java):
@Slf4j
public class PeacefulState implements State {
private final Mammoth mammoth;
public PeacefulState(Mammoth mammoth) {
this.mammoth = mammoth;
}
@Override
public void observe() {
LOGGER.info("{} is calm and peaceful.", mammoth);
}
@Override
public void onEnterState() {
LOGGER.info("{} calms down.", mammoth);
}
}
AngryState.java (state/src/main/java/com/iluwatar/state/AngryState.java):
@Slf4j
public class AngryState implements State {
private final Mammoth mammoth;
public AngryState(Mammoth mammoth) {
this.mammoth = mammoth;
}
@Override
public void observe() {
LOGGER.info("{} is furious!", mammoth);
}
@Override
public void onEnterState() {
LOGGER.info("{} gets angry!", mammoth);
}
}
Implement the Context Class
The context class (Mammoth.java in state/src/main/java/com/iluwatar/state/Mammoth.java) maintains the current state reference and handles transitions. It delegates behavior to the state object and encapsulates the transition logic.
public class Mammoth {
private State state;
public Mammoth() {
state = new PeacefulState(this); // initial state
}
// Simulates the passage of time and flips the mood
public void timePasses() {
if (state.getClass().equals(PeacefulState.class)) {
changeStateTo(new AngryState(this));
} else {
changeStateTo(new PeacefulState(this));
}
}
private void changeStateTo(State newState) {
this.state = newState;
this.state.onEnterState(); // hook for entry actions
}
public void observe() {
this.state.observe(); // delegate to current state
}
}
Handle State Transitions
Client code interacts with the context without knowing the specific state implementation. The timePasses() method triggers transitions, and the behavior changes dynamically.
public static void main(String[] args) {
var mammoth = new Mammoth();
mammoth.observe(); // calm
mammoth.timePasses(); // becomes angry
mammoth.observe(); // furious
mammoth.timePasses(); // back to calm
mammoth.observe(); // calm again
}
Output:
The mammoth calms down.
The mammoth is calm and peaceful.
The mammoth gets angry!
The mammoth is furious!
The mammoth calms down.
The mammoth is calm and peaceful.
Benefits of Using the State Pattern
Implementing the State pattern for object behavior transitions in Java provides several architectural advantages over traditional conditional approaches.
- Eliminates conditional complexity: Instead of
switchstatements orif-elsechains checking state variables, behavior is encapsulated in separate classes. - Encapsulates state-specific behavior: Each state class contains only the logic relevant to that state, improving cohesion and readability.
- Open/Closed compliance: Adding new states (such as
SleepStateorHungryState) requires only creating new classes that implementState; theMammothcontext requires no modification beyond the specific transition rule. - Dynamic behavior switching: The context can switch states at runtime through methods like
timePasses(), instantly changing observable behavior without changing the context's public interface.
Summary
- The State pattern delegates behavior to state objects, allowing an object to change its behavior when its internal state changes.
- The implementation requires a State interface (
State.java), concrete state classes (PeacefulState.java,AngryState.java), and a context class (Mammoth.java) that maintains the current state. - State transitions are handled within the context via methods like
changeStateTo(), which updates the state reference and triggers entry actions viaonEnterState(). - This approach eliminates complex conditional logic, adheres to the Open/Closed Principle, and makes the codebase more maintainable and extensible.
Frequently Asked Questions
What is the difference between the State pattern and the Strategy pattern?
Both patterns use composition to delegate behavior to separate classes, but their intent differs. The State pattern manages state-specific behavior transitions where the object changes its behavior based on internal state changes, and the context often manages transitions between states. The Strategy pattern is used to select an algorithm at runtime based on client choice, where strategies are typically independent and interchangeable without the context managing transitions between them.
How do you handle state transitions in the State pattern?
State transitions can be handled in two ways: either the Context class manages transitions (as shown in Mammoth.java where timePasses() decides when to switch states), or the Concrete State classes themselves trigger transitions by calling back into the context. The approach used in the java-design-patterns implementation keeps transition logic centralized in the context, which prevents states from having dependencies on each other and makes the transition rules easier to modify.
Can you add new states without modifying existing code?
Yes, the State pattern supports the Open/Closed Principle. You can add new states by creating new classes that implement the State interface without touching existing state classes. However, you may need to modify the Context class to include the new state in the transition graph—for example, adding a new condition in timePasses() or creating a new transition method. If you want to avoid modifying the context entirely, you can implement transitions within the state classes themselves, though this increases coupling between states.
What are the advantages of using onEnterState() callbacks?
The onEnterState() method provides a hook for entry actions that execute immediately when a state becomes active. This is useful for initialization logic, logging state changes, or triggering side effects that should occur every time the system enters a particular state. In the Mammoth example, this method logs mood changes ("calms down" or "gets angry"), providing clear visibility into state transitions without cluttering the business logic in the context or client code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →