Java State
Sometimes you need to create a class whose behavior depends on the current state of its object. If there are few states and no complex transitions between them, an if-else statement or a switch statement will be enough. But as the number of states grows and their logic becomes more complex, the code of the class may start looking more like spaghetti and hard to extend. The State design pattern might help you avoid such mishaps.
The problem
Imagine that you are writing a program simulating the interaction between an observer and a wild animal. Suppose that the animal can be in one of the two states; calm and angry, and react to the observer depending on the state it is currently in. We must keep in mind that later we may want to simulate the animal's behavior in greater detail and add more states, such as frightened, hungry, tired, etc.
First, we'll determine the animal's behavior in different states and how it transitions from one state to another. Let's presume that when the animal is calm, it rests in the grass. And when it is angry, it chases the observer. For the sake of simplicity, let's determine that the current state of the animal changes after a while. We will represent it using the following class:
class Animal {
private String state = "calm";
public void observe() {
if (state.equals("calm")) {
System.out.println(this + " is lying in the grass.");
} else {
System.out.println(this + " is chasing you. Run away!");
}
}
public void onTimePassed() {
if (state.equals("calm")) {
state = "angry";
} else {
state = "calm";
}
}
@Override
public String toString() {
return "Animal";
}
}
Now let's test our Animal
implementation:
class Main {
public static void main(String[] args) {
Animal animal = new Animal();
for (int i = 0; i < 4; i++) {
animal.observe();
animal.onTimePassed();
}
}
}
We will get this output:
Animal is lying in the grass.
Animal is chasing you. Run away!
Animal is lying in the grass.
Animal is chasing you. Run away!
Not bad! But we are going to have some issues if we try to extend this class with new behavior, like adding more states and/or announcements of state changes. If the behavior gets more complex, the class will grow in size. If the behavior gets more diverse, the class will have too many responsibilities. This is where the State design pattern comes to our rescue.
The idea of a pattern
The State design pattern helps us avoid the above problem. The idea is to introduce an abstraction that will represent different states of a class. The abstraction will describe the common interface for classes encapsulating the logic of each state. This will allow an instance of the class to vary its behavior depending on its state. For clients, it will look like the class of the object changes during runtime. The image below shows a UML diagram of the State pattern.
Let's go through all elements of this diagram. Context
defines an interface for interaction with clients and stores a reference to a ConcreteState
object that represents the current state of Context
. State
defines an interface for encapsulating the behavior associated with a concrete state of Context
. ConcreteState
classes implement the State
interface and determine the behavior associated with a certain concrete state of Context
. The transitions between the states may be defined either in Context
or in ConcreteState
classes. The latter option is more flexible but requires ConcreteState
to have a reference to Context
in order to set its current state. It also requires any ConcreteState
class to be aware of at least one other ConcreteState
.
Now let's apply the State pattern for our Animal
class and extend it a bit by adding announcements when the Animal
object enters a new state.
Pattern implementation
First, we define a common interface for different states. This interface will correspond to State
in the diagram we looked at in the previous section:
interface State {
void observe(); // used to observe the current behavior of Animal
void onStateEntry(); // invoked when the state changes
void onTimePassed(); // triggers the change of the state
}
The Animal
class will correspond to Context
in the diagram. We will add a State state
field to it for storing the current state and the corresponding setter. We will delegate the invocations of the observe
and onTimePassed
methods to the corresponding methods of its current state object.
class Animal {
private State state;
public void observe() {
state.observe();
}
public void onTimePassed() {
state.onTimePassed();
}
public void setState(State state) {
if (this.state == null) {
this.state = state; // no announcement on initial assignment
} else {
this.state = state;
this.state.onStateEntry();
}
}
@Override
public String toString() {
return "Animal";
}
}
After that, we create implementations of the State
interface for the two states. Each of them will correspond to a ConcreteState
in the diagram. In this example, we choose to let the state objects define when and where their transition to another state happens.
Here is the implementation of the calm state:
class CalmState implements State {
private final Animal animal;
CalmState(Animal animal) {
this.animal = animal;
}
@Override
public void observe() {
System.out.println(animal + " is lying in the grass.");
}
@Override
public void onStateEntry() {
System.out.println(animal + " calms down and stops.");
}
@Override
public void onTimePassed() {
animal.setState(new AngryState(animal));
}
}
And here is a code example of the angry state:
class AngryState implements State {
private final Animal animal;
AngryState(Animal animal) {
this.animal = animal;
}
@Override
public void observe() {
System.out.println(animal + " is chasing you. Run away!");
}
@Override
public void onStateEntry() {
System.out.println(animal + " has spotted you.");
}
@Override
public void onTimePassed() {
animal.setState(new CalmState(animal));
}
}
Now we are finally ready to test our new implementation!
Showcase
Let's test the behavior of the Animal
class the same way we did before:
class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.setState(new CalmState(animal));
for (int i = 0; i < 4; i++) {
animal.observe();
animal.onTimePassed();
}
}
}
Now let's run this code and see the output (this time we will have announcements displayed when the animal's state changes):
Animal is lying in the grass.
Animal has spotted you.
Animal is chasing you. Run away!
Animal calms down and stops.
Animal is lying in the grass.
Animal has spotted you.
Animal is chasing you. Run away!
Animal calms down and stops.
We can extend the animal's behavior by adding other implementations of State
and defining their transitions.
Conclusion
The State pattern is useful when the behavior of your class depends on its current state and changes during runtime, or when the methods of your class have a lot of if-else statements. However, this pattern requires you to create extra interfaces and implementations and may lead to excessive code complexity if your class has few states which change rarely. If used in appropriate cases, this pattern allows for easy extension of the class by adding more states represented by new classes that encapsulate the necessary behavior.