State design pattern

The state design pattern is used to model the state machines. The transitions between states are usually handled in a big switch statement. This results in complex switch statement and can lead to testing difficulties. State pattern helps in addressing the problem using object-oriented approach.

Each state is modeled as a separate state. For example, consider a file object. File can be either opened, closed or deleted. Instead of modeling the states as enum, each state is modeled as a concrete object. An abstract class is used to define the common interface for all the states. The controller that is utilizing the object just calls the common interface.

The creation of new states can be in either controller or within the states itself. A typical File class is:

class File {

    ...
    private FileState fileState;
    ...
    void openFile() {
        switch (fileState) {
            case OPEN: System.out.println("Already opened!");
            case CLOSED: this.fileState = OPEN;
            case DELETED: throw new FileAlreadyDeletedException();
        }
    }
    ...
}

In this implementation, the logic is quite simple. In real-world examples, these can be quite extensive with many guard rails for each state. Hence, we remodel this using state pattern.

A FileState abstract class can be:

abstract class FileState {

    ...
    abstract void open(File file);
    abstract void close(File file);
    abstract void delete(File file);
    ...

}

The File class will delegate all the calls to this object.

class File {

    ...
    private FileState fileState;
    ...
    void openFile() {
        this.fileState.open(this);
    }

    void closeFile() {
        this.fileState.close(this);
    }

    void deleteFile() {
        this.fileState.delete(this);
    }

    // A helper message to update the fileState object.
    void updateState(FileState newState) {
        this.fileState = newState;
    }
    ...
}

Now what remains to do is the concrete implementations for each state. OpenState can have something like this.

class OpenState extends FileState {

    ...
    @Override
    void open(File file) {
        System.out.println("Already opened!");
    }

    @Override
    void close(File file) {
        file.updateState(new CloseState()); // We are updating the file with the new state.
    }

    @Override
    void delete(File file) {
        System.out.println("Cannot delete when the file is open!"); // Invalid state transitions are prohibited.
    }
    ...
}

Similar implementations can be done for CloseState and DeleteState as well. In CloseState, we can allow file deletion and opening. In DeleteState, we can throw errors for both opening and closing.

Additionally, we can add guardrails to state transitions. For instance, OpenState can have a check for permissions before trying to open the file. File should pass these necessary constraints to the state objects, which can then use these guard rails and do additional conditional transitions.




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Vendor lock-in Antipattern
  • Cut and Paste Programming Antipattern
  • Spaghetti Antipattern