Skip to content

Unraveling the State Design Pattern

Posted on:September 21, 2022 at 01:13 PM

In this blog post, we’re going to explore the State Design Pattern. The State Design Pattern is a powerful way to manage an object’s behavior as it transitions through a finite number of states. We’ll dive into the concept, key components, advantages, and a practical example of this pattern.

Table of contents

Open Table of contents

Sections

Understanding Finite States

Imagine a scenario where you’re working on a blog post. The post can go through several states:

Draft:

The initial stage where you create your article.

Moderation:

After drafting, someone reviews the article for quality and appropriateness.

Published:

The final stage where the article becomes available to readers.

These states can be connected, but there are limitations. For instance, you can’t transition directly from “Published” back to “Moderation.” These transitions depend on how you define the state graph.

The State Design Pattern is all about encapsulating these states within objects and switching between them based on the current state, allowing for different functionality at each stage.

stateGraphicProblem

Key Components of the State Design Pattern

Context:

The context is the object that can change its behavior based on the internal state. In our example, it’s the article or blog post.

States:

Each state is represented by a class that implements a common abstract Class. In our example, we’d have “Draft,” “Moderation,” and “Published” states as separate classes, each implementing a specific set of behaviors.

Advantages of Using the State Design Pattern

Clarity and Maintainability:

It enhances code organization and makes it easier to maintain by encapsulating state-specific behaviors within dedicated state classes.

Flexibility:

The pattern facilitates state transitions and enables you to add new states, making it suitable for dynamic systems.

Readability:

This pattern contributes to code readability, as each state’s behavior resides in a distinct class, making it evident which functionalities belong to which state.

Scalability:

As your application grows, you can effortlessly add new states without altering existing code.

Testing:

Testing becomes more focused and efficient since you can isolate and test each state separately.

Practical Implementation: A Simple Game Example

Let’s demonstrate the State Design Pattern through a basic game example. In this game, there are four possible states: “Welcome Screen,” “Playing,” “Pause,” and “End Game.” Each state has specific behaviors, and the game transitions between these states. Below is a simplified implementation of this pattern:

Abstract Class State

public abstract class State {
    Game game;

    public State(Game game) {
        this.game = game;
    }

    public abstract void  onWelcomeScreen();
    public abstract void  onPlaying();
    public abstract void  onBreak();
    public abstract void  onEndGame();
}

Game Class (Context)

public class Game  {
    public State state = new WelcomeScreenState(this);

    public void changeState(State state) {
        this.state = state;

    }
}

WelcomeScreenState Class

public class WelcomeScreenState extends State {
    public WelcomeScreenState(Game game) {
        super(game);
        System.out.println("--- Game in Welcome Screen State ---");
    }

    @Override
    public void onWelcomeScreen() {
        System.out.println("Currently on Welcome Screen");
    }

    @Override
    public void onPlaying() {
        game.changeState(new PlayingState(game));
    }

    @Override
    public void onBreak() {
        System.out.println("Not Allowed");
    }

    @Override
    public void onEndGame() {
        System.out.println("Not Allowed");
    }
}

PlayingState Class

public class PlayingState extends State{
    public PlayingState(Game game) {
        super(game);
        System.out.println("--- Game in Playing State ---");
    }

    @Override
    public void onWelcomeScreen() {
        System.out.println("Not Allowed");
    }

    @Override
    public void onPlaying() {
        System.out.println("Currently playing!");
    }

    @Override
    public void onBreak() {
        game.changeState(new BreakState(game));
    }

    @Override
    public void onEndGame() {
        game.changeState(new EndGame(game));

    }
}

BreakState Class

public class BreakState extends State {
    public BreakState(Game game) {
        super(game);
        System.out.println("--- Game in Break State ---");
    }

    @Override
    public void onWelcomeScreen() {
        System.out.println("Not Allowed!");
    }

    @Override
    public void onPlaying() {
        game.changeState(new PlayingState(game));
    }

    @Override
    public void onBreak() {
        System.out.println("Currently on Break");
    }

    @Override
    public void onEndGame() {
        System.out.println("Not Allowed!");;
    }
}

EndGameState Class

public class EndGame extends State {
    public EndGame(Game game) {
        super(game);
        System.out.println("--- Game in End Game State ---");
    }

    @Override
    public void onWelcomeScreen() {
        game.changeState(new WelcomeScreenState(game));
    }

    @Override
    public void onPlaying() {
        System.out.println("Not Allowed!");
    }

    @Override
    public void onBreak() {
        System.out.println("Not Allowed!");
    }

    @Override
    public void onEndGame() {
        System.out.println("Currently EndGameState!");
    }
}

Client Code

public class Client {
    public static void main(String[] args) {
        Game game = new Game();
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String input = "";

        do {
            System.out.print("--- Please Input Commands ---");
            try {
                input = reader.readLine().trim().toLowerCase();
                switch (input) {
                    case "w": game.state.onWelcomeScreen();
                        break;
                    case "p": game.state.onPlaying();
                        break;
                    case "b": game.state.onBreak();
                        break;
                    case "e": game.state.onEndGame();
                        break;
                    default: System.out.println("-- Unknown Command ---");
                        break;
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        } while (input != "exit");
    }
}

In this example, we have the main game class (context), which manages the state and can switch between states. Each state is represented by a class that implements the state interface and offers state-specific behavior. A client class reads user input and instructs the game to switch states based on user commands (e.g., “W” for “Welcome Screen”).

Conclusion

The State Design Pattern is a powerful tool for modeling systems with finite states, providing clarity, flexibility, and maintainability in your code. By encapsulating behavior within state classes and enabling seamless state transitions, you can build more dynamic and adaptable applications. The example we’ve provided illustrates the essence of this pattern and its practical utility in real-world scenarios.

This article should serve as a comprehensive introduction to the State Design Pattern. It’s a versatile concept that you can apply in various domains to manage complexity and improve the maintainability of your code.

Happy coding! 🚀