When it’s necessary to maintain the state from an object we can create a big code full of ifs controlling the State from the class. Certainly, this is not the right approach to solving this problem. Repetition of code is what we must avoid, we must follow the DRY (Don’t repeat yourself) principle to keep the code flexible and powerful.
Get the Design_Patterns_Saga_GitHub_Source_Code
Let’s first see a bad example of a “State” implementation.
1 – State Bad Example: There are two constants, OPEN and CLOSED. Whenever the openWindow or closeWindow methods are invoked all the rules will be manipulated inside them. As you can see in the code below, there is repetition.
public class AutomaticWindow { final static int CLOSED = 0; final static int OPEN = 1; int state = CLOSED; public AutomaticWindow() { super(); } public void openWindow() { if (state == OPEN) { System.out.println("Window is already open"); } else if (state == CLOSED) { System.out.println("Opening window."); state = OPEN; } } public void closeWindow() { if (state == OPEN) { System.out.println("Closing window."); state = CLOSED; } else if (state == CLOSED) { System.out.println("Window is already closed."); } } public String toString() { if (state == OPEN) { return "Window is open"; } else { return "Window is closed"; } } }
Let’s now see how to use the State pattern for real! You will see the difference between a much cleaner and more flexible code.
1 – State: It is the generic abstract class that handles the request from any class that must keep the State.
public abstract class State { public abstract void handleRequest(); }
2 – State concrete classes: They are basically the classes that extend the State class. They are responsible for changing the State to the next one in the sequence.
public class GreenTrafficLightState extends State { private TrafficLight trafficLight; public GreenTrafficLightState(TrafficLight trafficLight) { this.trafficLight = trafficLight; } @Override public void handleRequest() { System.out.println("Turning traffic light to yellow."); trafficLight.setState(trafficLight.getYellowLightState()); } public String toString() { return "Traffic light is green."; } } public class RedTrafficLightState extends State { private TrafficLight trafficLight; public RedTrafficLightState(TrafficLight trafficLight) { this.trafficLight = trafficLight; } @Override public void handleRequest() { System.out.println("Turning traffic light to green..."); trafficLight.setState(trafficLight.getGreenLightState()); } public String toString() { return "Traffic light is on red."; } } public class YellowTrafficLightState extends State { private TrafficLight trafficLight; public YellowTrafficLightState(TrafficLight trafficLight) { this.trafficLight = trafficLight; } @Override public void handleRequest() { System.out.println("Turning traffic light to red."); trafficLight.setState(trafficLight.getRedLightState()); } public String toString() { return "Traffic light is yellow."; } }
3 – Orchestrator: This class is responsible for orchestrating the changes in the traffic lights State. In the constructor, we initialize all the States and we pass the same instance in the constructor to each one of the States classes. In the changeState method, we just delegate the call to the State instance variable to change the State.
public class TrafficLight { State red; State yellow; State green; State state; public TrafficLight() { red = new RedTrafficLightState(this); yellow = new YellowTrafficLightState(this); green = new GreenTrafficLightState(this); state = red; } public void changeState() { state.handleRequest(); } public String toString() { return state.toString(); } public State getGreenLightState() { return green; } public State getYellowLightState() { return yellow; } public State getRedLightState() { return red; } public void setState(State state) { this.state = state; } }
4- Unit Tests: Now let’s see if the states are changing as expected. We are expecting traffic lights to change from red to green, yellow to red, and so forth. In the window case, we want it to open and close.
public class StateTest { @Test public void stateTest() { TrafficLight trafficLight = new TrafficLight(); trafficLight.changeState(); trafficLight.changeState(); trafficLight.changeState(); trafficLight.changeState(); Assert.assertEquals(trafficLight.state.getClass(), GreenTrafficLightState.class); } @Test public void badExampleStateTest() { AutomaticWindow automaticWindow = new AutomaticWindow(); automaticWindow.openWindow(); automaticWindow.closeWindow(); Assert.assertEquals(automaticWindow.toString(),"Window is closed"); } }
Summary of actions:
- Created the State abstract generic class.
- Created the State classes that extend the State class.
- Implemented the handleRequest method from the State class.
- Created the TrafficLight class to manipulate the States.
- Created the changeState method in the TrafficLight class.
To practice the State Pattern you can create another State class with another scenario, for example changing any light state or anything else. Keep in mind that practicing the Patterns is crucial to fully master them. Be sure to use TDD (Test Driven Development).