Introduce
In fact, there are many problems related to states, such as order status, playing music status, content status through processes … In cases like this, It is impossible not to mention the State Design Pattern, this design pattern will help us to design the structure, handle the state-related logic more easily and professionally. Today we will learn about State Design Pattern (State Design Pattern), specifically how it works, how to implement this design pattern in Java.
First, we will give an overview of its purpose and explain the problem it tries to solve. Later, we’ll find its UML chart type and implement the actual example.
Overview of State Design Pattern
The main idea of the State design pattern is that it allows objects to change their behavior and state without changing its class. Also, by implementing it, the code will be cleaner and more beautiful without the need for many if / else statements.
Imagine we have a package ( package
) is sent to the post office, the package can be ordered ( order
), then sent to the post office and get the last customer. Now, depending on the actual state, we want to print its delivery status.
The simplest approach is to add some boolean flags and apply simple if / other statements in each of our methods in the class. That won’t complicate it much in a simple scenario. However, it can be complicated and confusing in our code as we will handle more states, which will result in more if / other statements.
In addition, all the logic for each state is spread evenly across all methods. Now, this is where State designs can be considered. Thanks to the State design pattern, we can encapsulate the logic in specialized classes, apply the Single Responsibility Principle and the Open-closed principle , the code will be cleaner and easier to maintain.
UML chart
Take a look at the UML diagram for this design pattern:
In the UML diagram, we see that the Context
class has a linked State
that changes during program execution.
Context
will delegate state execution behavior. In other words, all behaviors will be handled by a specific implementation ( handle
method) of the state.
We see that the logic is separated and adding new states is simple – it is also possible to add other states if needed.
Implementation
Now let’s learn how to implement this design, as mentioned above our object is the package ( package
), which has the following status: order ( order
), delivery ( delivered
) and receive the goods ( received
). So we will have 3 states for the context class (here is the package
)
First, the context definition, in this particular example, is the Package
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Package { private PackageState state = new OrderedState(); // getter, setter public void previousState() { state.prev(this); } public void nextState() { state.next(this); } public void printStatus() { state.printStatus(); } } |
A little more interested in this class, our Package
class will contain a state object (it is PackageState
), PackageState
will be an interface representing the three states mentioned above: OrderedState
, DeliveredState
and ReceivedState
. The Package
class implements three methods related to the state: previousState
(revert to the previous state), nextState
(move to the next state) and printStatus
(notify the current status of the package), starting from OrderedState
status.
Next, we will have a PackageState
interface that has three methods as follows:
1 2 3 4 5 6 7 | public interface PackageState { void next(Package pkg); void prev(Package pkg); void printStatus(); } |
This interface will be implemented by 3 states corresponding to 3 classes: OrderedState
, DeliveredState
and ReceivedState
:
OrderedState
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class OrderedState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new DeliveredState()); } @Override public void prev(Package pkg) { System.out.println("The package is in its root state."); } @Override public void printStatus() { System.out.println("Package ordered, not delivered to the office yet."); } } |
OrderedState
is the first state of the package, it cannot be returned to the previous state, but can move to the next state which is DeliveredState
DeliveredState
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class DeliveredState implements PackageState { @Override public void next(Package pkg) { pkg.setState(new ReceivedState()); } @Override public void prev(Package pkg) { pkg.setState(new OrderedState()); } @Override public void printStatus() { System.out.println("Package delivered to post office, not received yet."); } } |
DeliveredState
is the second state of the package in the above example, it can revert to the previous state of OrderedState
, and can also switch to the next state ReceivedState
.
ReceivedState
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ReceivedState implements PackageState { @Override public void next(Package pkg) { System.out.println("This package is already received by a client."); } @Override public void prev(Package pkg) { pkg.setState(new DeliveredState()); } } |
ReceivedState
is the final state of the package, it can only return to the previous state DeliveredState
.
Testing
Let’s see how. First, verify that the setup transitions work as expected with a few small tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Test public void givenNewPackage_whenPackageReceived_thenStateReceived() { Package pkg = new Package(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(DeliveredState.class)); pkg.nextState(); assertThat(pkg.getState(), instanceOf(ReceivedState.class)); } |
Next, quickly check if our package can return to its status:
1 2 3 4 5 6 7 8 9 | @Test public void givenDeliveredPackage_whenPrevState_thenStateOrdered() { Package pkg = new Package(); pkg.setState(new DeliveredState()); pkg.previousState(); assertThat(pkg.getState(), instanceOf(OrderedState.class)); } |
Then, verify the state change and see how the implementation of the printStatus()
method changes its implementation at runtime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class StateDemo { public static void main(String[] args) { Package pkg = new Package(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); pkg.nextState(); pkg.printStatus(); } } |
And here is the output:
1 2 3 4 5 6 | Package ordered, not delivered to the office yet. Package delivered to post office, not received yet. Package was received by client. This package is already received by a client. Package was received by client. |
Limit
State template restriction is the implementation of transitioning between states. That makes the state hardcoded, which is a generally bad practice.
However, depending on our needs and requirements, it may or may not be a problem.
Conclude
State designs are great when we want to avoid common if / else statements. Instead, we extract the logic to separate the classes and let our context object delegate behavior to the methods implemented in the state class. Besides, we can take advantage of transitions between states, in which a state can change the state of the context.