State Pattern: Transforming Objects Through Internal States

In software development, we often encounter objects whose behavior must change based on their internal state. Think of a document (draft, under review, published), an order (new, paid, shipped, delivered), or as we'll explore, a student moving through academic stages. However, with the advancement of programming and the complexity of the problems we face, we discovered that every object has two types of attributes: static attributes and state attributes , which will be the focus of our discussion now. Because these attributes have behavior and state rather than a fixed value, dealing with them can be complex. Changing an object's state may result in a change in its behavior. For example, a person's behavior when they were children differs from their behavior when they became adolescents, even though they remain the same "human." Therefore, it was found preferable to isolate these attributes in an abstract interface. Within the abstract interface, there are functions known as state functions which cause states to change and are similar to rules. In some cases, they are called state operator functions Each state then executes these functions, but according to the implementation appropriate for that state. When handled improperly, state-dependent behavior leads to complex, difficult-to-maintain code filled with conditional statements. The State Pattern offers an elegant solution to this problem. What is the State Pattern? The State Pattern allows an object to change its behavior when its internal state changes. The pattern makes the object appear to change its class by encapsulating state-specific behavior in separate state classes and eliminating complex conditional statements. As defined by the Gang of Four, this pattern: Encapsulates state-specific behavior in separate classes Delegates state-specific behavior to the current state object Allows for easy addition of new states with minimal changes to existing code The Problem: Evolving Student States Let's consider a concrete example: a Student class that handles different behaviors as the student progresses through exam preparation: Preparing: Student is studying for the exam ExamTaken: Student has taken the exam and is awaiting results Passed: Student has passed and completed the course Traditional Approach with Conditionals Here's how this might be implemented traditionally: public class Student { // State constants public static final int PREPARING = 0; public static final int EXAM_TAKEN = 1; public static final int PASSED = 2; private int state; private String name; public Student(String name) { this.name = name; // Initial state is PREPARING this.state = PREPARING; } public void study() { System.out.println(name + " is attempting to study."); if (state == PREPARING) { System.out.println("Studying harder to be prepared for the exam!"); } else if (state == EXAM_TAKEN) { System.out.println("Already took the exam! Waiting for results..."); } else if (state == PASSED) { System.out.println("Studying for the next course!"); } else { System.out.println("Unknown state!"); } } public void takeExam() { System.out.println(name + " is attempting to take the exam."); if (state == PREPARING) { System.out.println("Taking the exam now..."); state = EXAM_TAKEN; } else if (state == EXAM_TAKEN) { System.out.println("Cannot take the exam again! Already taken."); } else if (state == PASSED) { System.out.println("Already passed this exam! Moving to next course."); } else { System.out.println("Unknown state!"); } } public void receiveResults(boolean passed) { System.out.println(name + " is receiving exam results."); if (state == PREPARING) { System.out.println("Cannot receive results yet - haven't taken the exam!"); } else if (state == EXAM_TAKEN) { System.out.println("Receiving exam results..."); if (passed) { state = PASSED; System.out.println("Passed the exam!"); } else { state = PREPARING; System.out.println("Failed the exam. Need to prepare again!"); } } else if (state == PASSED) { System.out.println("Already received the results. Passed!"); } else { System.out.println("Unknown state!"); } } public String getStateName() { if (state == PREPARING) { return "Preparing for exam"; } else if (state == EXAM_TAKEN) { return "Waiting for results"; } else if (state == PASSED) { return "Passed the exam"; } else { return "Unknown"; } } @Override

May 9, 2025 - 11:07
 0
State Pattern: Transforming Objects Through Internal States

In software development, we often encounter objects whose behavior must change based on their internal state. Think of a document (draft, under review, published), an order (new, paid, shipped, delivered), or as we'll explore, a student moving through academic stages.

However, with the advancement of programming and the complexity of the problems we face, we discovered that every object has two types of attributes: static attributes and state attributes , which will be the focus of our discussion now. Because these attributes have behavior and state rather than a fixed value, dealing with them can be complex. Changing an object's state may result in a change in its behavior. For example, a person's behavior when they were children differs from their behavior when they became adolescents, even though they remain the same "human."

Therefore, it was found preferable to isolate these attributes in an abstract interface. Within the abstract interface, there are functions known as state functions which cause states to change and are similar to rules. In some cases, they are called state operator functions Each state then executes these functions, but according to the implementation appropriate for that state.

When handled improperly, state-dependent behavior leads to complex, difficult-to-maintain code filled with conditional statements. The State Pattern offers an elegant solution to this problem.

What is the State Pattern?

The State Pattern allows an object to change its behavior when its internal state changes. The pattern makes the object appear to change its class by encapsulating state-specific behavior in separate state classes and eliminating complex conditional statements.

As defined by the Gang of Four, this pattern:

  • Encapsulates state-specific behavior in separate classes
  • Delegates state-specific behavior to the current state object
  • Allows for easy addition of new states with minimal changes to existing code

The Problem: Evolving Student States

Image description

Let's consider a concrete example: a Student class that handles different behaviors as the student progresses through exam preparation:

  1. Preparing: Student is studying for the exam
  2. ExamTaken: Student has taken the exam and is awaiting results
  3. Passed: Student has passed and completed the course

Traditional Approach with Conditionals

Here's how this might be implemented traditionally:

public class Student {
    // State constants
    public static final int PREPARING = 0;
    public static final int EXAM_TAKEN = 1;
    public static final int PASSED = 2;

    private int state;
    private String name;

    public Student(String name) {
        this.name = name;
        // Initial state is PREPARING
        this.state = PREPARING;
    }

    public void study() {
        System.out.println(name + " is attempting to study.");

        if (state == PREPARING) {
            System.out.println("Studying harder to be prepared for the exam!");
        } else if (state == EXAM_TAKEN) {
            System.out.println("Already took the exam! Waiting for results...");
        } else if (state == PASSED) {
            System.out.println("Studying for the next course!");
        } else {
            System.out.println("Unknown state!");
        }
    }

    public void takeExam() {
        System.out.println(name + " is attempting to take the exam.");

        if (state == PREPARING) {
            System.out.println("Taking the exam now...");
            state = EXAM_TAKEN;
        } else if (state == EXAM_TAKEN) {
            System.out.println("Cannot take the exam again! Already taken.");
        } else if (state == PASSED) {
            System.out.println("Already passed this exam! Moving to next course.");
        } else {
            System.out.println("Unknown state!");
        }
    }

    public void receiveResults(boolean passed) {
        System.out.println(name + " is receiving exam results.");

        if (state == PREPARING) {
            System.out.println("Cannot receive results yet - haven't taken the exam!");
        } else if (state == EXAM_TAKEN) {
            System.out.println("Receiving exam results...");
            if (passed) {
                state = PASSED;
                System.out.println("Passed the exam!");
            } else {
                state = PREPARING;
                System.out.println("Failed the exam. Need to prepare again!");
            }
        } else if (state == PASSED) {
            System.out.println("Already received the results. Passed!");
        } else {
            System.out.println("Unknown state!");
        }
    }

    public String getStateName() {
        if (state == PREPARING) {
            return "Preparing for exam";
        } else if (state == EXAM_TAKEN) {
            return "Waiting for results";
        } else if (state == PASSED) {
            return "Passed the exam";
        } else {
            return "Unknown";
        }
    }

    @Override
    public String toString() {
        return name + " - Current state: " + getStateName();
    }
}

Problems with the Traditional Approach

This implementation has several issues:

  1. Code Duplication: The same state checks are repeated in every method
  2. Complex Conditionals: Each method contains multiple if-else branches
  3. Scattered State Transitions: State changes are dispersed throughout the code
  4. Poor Maintainability: Adding a new state requires modifying every method
  5. Error Prone: Easy to miss updating a condition when adding states
  6. Hard to Read: Understanding all possible state transitions is difficult
  7. Violation of OCP: The class must be modified to add new states
  8. Violation of SRP: The Student class is responsible for all state behaviors

The Solution: Applying the State Pattern

Instead of managing state transitions with conditionals, we can use the State Pattern to separate each state's behavior into its own class:

1. Define the State Interface

interface StudentState {
    void study(Student student);
    void takeExam(Student student);
    void receiveResults(Student student, boolean passed);
    String getStateName();
}

2. Implement Concrete States

class PreparingState implements StudentState {
    @Override
    public void study(Student student) {
        System.out.println("Studying harder to be prepared for the exam!");
    }

    @Override
    public void takeExam(Student student) {
        System.out.println("Taking the exam now...");
        student.setState(new ExamTakenState());
    }

    @Override
    public void receiveResults(Student student, boolean passed) {
        System.out.println("Cannot receive results yet - haven't taken the exam!");
    }

    @Override
    public String getStateName() {
        return "Preparing for exam";
    }
}

class ExamTakenState implements StudentState {
    @Override
    public void study(Student student) {
        System.out.println("Already took the exam! Waiting for results...");
    }

    @Override
    public void takeExam(Student student) {
        System.out.println("Cannot take the exam again! Already taken.");
    }

    @Override
    public void receiveResults(Student student, boolean passed) {
        System.out.println("Receiving exam results...");
        if (passed) {
            student.setState(new PassedState());
            System.out.println("Passed the exam!");
        } else {
            student.setState(new PreparingState());
            System.out.println("Failed the exam. Need to prepare again!");
        }
    }

    @Override
    public String getStateName() {
        return "Waiting for results";
    }
}

class PassedState implements StudentState {
    @Override
    public void study(Student student) {
        System.out.println("Studying for the next course!");
    }

    @Override
    public void takeExam(Student student) {
        System.out.println("Already passed this exam! Moving to next course.");
    }

    @Override
    public void receiveResults(Student student, boolean passed) {
        System.out.println("Already received the results. Passed!");
    }

    @Override
    public String getStateName() {
        return "Passed the exam";
    }
}

3. Refactor the Context Class

class Student {
    private StudentState state;
    private String name;

    public Student(String name) {
        this.name = name;
        // Initial state is Preparing
        this.state = new PreparingState();
    }

    public void setState(StudentState state) {
        this.state = state;
    }

    public void study() {
        System.out.println(name + " is attempting to study.");
        state.study(this);
    }

    public void takeExam() {
        System.out.println(name + " is attempting to take the exam.");
        state.takeExam(this);
    }

    public void receiveResults(boolean passed) {
        System.out.println(name + " is receiving exam results.");
        state.receiveResults(this, passed);
    }

    public String getStateName() {
        return state.getStateName();
    }

    @Override
    public String toString() {
        return name + " - Current state: " + state.getStateName();
    }
}

Benefits of the State Pattern

  1. Clean Code: Eliminates messy conditionals
  2. Maintainability: State-specific code is localized
  3. Extensibility: New states can be added easily
  4. Flexibility: States can be swapped at runtime
  5. Testability: Each state can be tested in isolation
  6. Readability: State transitions are explicit
  7. Single Responsibility: Each state class has one purpose
  8. Open-Closed Principle: Student class is closed for modification but open for extension

State Pattern vs. Strategy Pattern: Key Differences

The State and Strategy patterns share a similar structure but have different intents:

Aspect Strategy Pattern State Pattern
Intent Defines a family of interchangeable algorithms Changes object behavior when internal state changes
Control Client code selects strategy States often control transitions to other states
Relationship Strategies typically independent States often know about and reference each other
Example Client decides duck's flying behavior Student automatically changes own state

When to Use the State Pattern

The State Pattern is particularly useful when:

  • An object's behavior depends on its state
  • You have multiple operations with state-dependent behavior
  • State transitions follow well-defined rules
  • You need to eliminate complex conditional logic

It might be overkill when:

  • You have few states or simple transitions
  • States rarely change
  • State-specific behavior is minimal

Real-World Examples

The State Pattern appears in many modern frameworks and libraries:

  1. XState: JavaScript library implementing formal state machines to manage complex UI state transitions
  2. RxJava: Uses Observer pattern primarily but incorporates state management for asynchronous stream processing
  3. Unidraw: Framework for graphical editors that employs state-like behavior for tool switching

Conclusion

The State Pattern is a powerful tool for managing complex state-dependent behavior. By encapsulating each state in its own class, you create code that's easier to understand, test, and extend. Next time you find yourself writing a growing number of state-checking conditionals, consider if the State Pattern might provide a more elegant solution.

This article is inspired by concepts from "Head First Design Patterns" by Eric Freeman & Elisabeth Robson and "Design Patterns: Elements of Reusable Object-Oriented Software" by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.