State Design Pattern In Java
State Design Pattern — a behavioral design pattern that allows an object to change its behavior when its internal state changes.
Join the DZone community and get the full member experience.
Join For FreeState Design Pattern — a behavioral design pattern that allows an object to change its behavior when its internal state changes.
State Design Pattern
- The State Design Pattern is a Behavioral Design Pattern and one of the Gang of Four design patterns.
- The State allows an object to alter its behavior when its internal state changes.
- The State pattern is similar to the concept of finite-state machines.
- The State pattern is also similar to the Strategy Design Pattern which provides a way to switch a strategy through invocations of methods defined in the pattern's interface.
- The State pattern encapsulates varying behavior for the object based on its internal state change.
- The State pattern provides a cleaner way for an object to change its behavior at runtime.
- By using the State pattern, the object changes its behavior when its internal state changes.
- If we implement State-Specific behavior directly in the class, then we will not be able to change it without modifying the class.
- In-State pattern, State-specific behavior should be defined independently because adding new states should not affect the behavior of existing states.
- The context class delegates state-specific behavior to its current state object instead of implementing state-specific behavior directly.
- This allows us to make our context class independent of how state-specific behavior is implemented. New state classes can be added without modifying context class.
- The context class can change its behavior at run-time by changing its current state object.
- To implement the State Design Pattern, we create a State interface to define some action. And then concrete classes that represent various states and a context object whose behavior varies as its state object changes.
- The mixer in the kitchen is a good example of a state pattern, which has a motor and a control interface. Using the knob we can increase/decrease the speed of the mixer. Based on the speed state the behavior changes.
- The TV which can be operated with a remote controller is another example of a State pattern. We can change the state of the TV by pressing buttons on the remote. But the state of TV will change or not, it depends on the current state of the TV. If the TV is switched OFF then only possible next state can be switch ON. And if TV is ON, we can switch it OFF, mute, or change aspects and source. But if TV is OFF, nothing will happen when we press the remote buttons.
- Java Threads are another good example of State pattern since they have defined states as New, Runnable, Blocked, Waiting, Timed Waiting and Terminated.
To understand this better — let's take an example of Shipment Processing where the order status changes from place to receive. There can also be exceptions while shipping and the customer can also return it if he doesn't like it. I will try to keep the example as simple as possible and aligned to the use of State Pattern.
Shipment Processing Example Using State Design Pattern
In this example, we mainly focus on the Shipment of Order. So, let's define a class called Shipment:
package org.trishinfotech.state;
public class Shipment {
protected String orderNumber;
protected String orderItem;
protected String shipmentNumber;
protected Location deliveryAddress;
public Shipment(String orderNumber, String orderItem, Location deliveryAddress) {
super();
this.orderNumber = orderNumber;
this.orderItem = orderItem;
this.deliveryAddress = deliveryAddress;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public String getOrderItem() {
return orderItem;
}
public void setOrderItem(String orderItem) {
this.orderItem = orderItem;
}
public String getShipmentNumber() {
return shipmentNumber;
}
public void setShipmentNumber(String shipmentNumber) {
this.shipmentNumber = shipmentNumber;
}
public Location getDeliveryAddress() {
return deliveryAddress;
}
public void setDeliveryAddress(Location deliveryAddress) {
this.deliveryAddress = deliveryAddress;
}
}
Since we need Location of shipment while we transport, let's define Location class:
xxxxxxxxxx
package org.trishinfotech.state;
public class Location {
protected String code;
protected String address;
protected String area;
protected String city;
protected String country;
protected String zipCode;
public Location(String code, String address, String area, String city, String country, String zipCode) {
super();
this.code = code;
this.address = address;
this.area = area;
this.city = city;
this.country = country;
this.zipCode = zipCode;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getArea() {
return area;
}
public void setArea(String area) {
this.area = area;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(code).append(" (").append(address).append(" ").append(area).append(" ").append(city).append(" ")
.append(country).append(" ").append(zipCode).append(")");
return builder.toString();
}
}
Now its time to define our state interface called ShipmentState which will allow us to have different shipment status (depending on our logic and input parameters) time to time.
xxxxxxxxxx
package org.trishinfotech.state;
public interface ShipmentState {
public String name();
public void processShipment(ShipmentContext context);
}
Now lets define our context class called ShipmentContext which will have changing Status and hence the changing behavior as well.
xxxxxxxxxx
package org.trishinfotech.state;
public class ShipmentContext {
protected Shipment shipment;
protected ShipmentState currentState;
protected boolean customerAtLocation = false;
protected int currentLocationIndex = 0;
public ShipmentContext(Shipment shipment) {
super();
this.shipment = shipment;
this.currentState = new OrderPlaced();
}
public Shipment getShipment() {
return shipment;
}
public void setShipment(Shipment shipment) {
this.shipment = shipment;
}
public ShipmentState getCurrentState() {
return currentState;
}
public void setCurrentState(ShipmentState currentState) {
this.currentState = currentState;
}
public int getCurrentLocationIndex() {
return currentLocationIndex;
}
public void setCurrentLocationIndex(int currentLocationIndex) {
this.currentLocationIndex = currentLocationIndex;
}
public boolean isCustomerAtLocation() {
return customerAtLocation;
}
public void setCustomerAtLocation(boolean customerAtLocation) {
this.customerAtLocation = customerAtLocation;
}
public void processShipment() {
currentState.processShipment(this);
}
}
Now we will define concrete status classes to represent different order/shipment status.
Below status class we will create:
- Code for OrderPlaced class:
xxxxxxxxxx
package org.trishinfotech.state;
public class OrderPlaced implements ShipmentState {
public OrderPlaced() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
context.setCurrentState(new ProcessingStock());
}
public String name() {
return "Order Placed";
}
}
- Code for ProcessingStock class:
xxxxxxxxxx
package org.trishinfotech.state;
public class ProcessingStock implements ShipmentState {
public ProcessingStock() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
context.setCurrentState(new ReadyForPacking());
}
public String name() {
return "Processing Stock";
}
}
- Code for ReadyForPacking class:
xxxxxxxxxx
package org.trishinfotech.state;
public class ReadyForPacking implements ShipmentState {
public ReadyForPacking() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
context.setCurrentState(new ReadyToDeliver());
}
public String name() {
return "Ready For Packing";
}
}
- Code for ReadyToDeliver class:
xxxxxxxxxx
package org.trishinfotech.state;
public class ReadyToDeliver implements ShipmentState {
public ReadyToDeliver() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
// shipment tracking number will be generated.
shipment.setShipmentNumber("ST6749398FLNY26");
Location originLocation = new Location("LocA", "54 Essex Rd.", "Palm Bay", "FL", "US", "32907");
context.setCurrentState(new DeliveryInProgress(ItineraryFinder.findItinerry(originLocation, context.getShipment().getDeliveryAddress())));
}
public String name() {
return "Ready To Deliver";
}
}
To make the example little interesting without making it complex, I added shipment itinerary required for moving the order from warehouse to the customer address. And to achieve that I wrote a class called ItineraryFinder which has hard-coded intermediate locations (routing/interchange points) while we move the order.
xxxxxxxxxx
package org.trishinfotech.state;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ItineraryFinder {
public static List<Location> findItinerry(Location originLocation, Location destinationLocation) {
// hard-coded itinerary locations to keep the code simple.
return Arrays.stream(new Location[] { originLocation,
new Location("LocB", "335 Hall Street", "Pelham", "AL", "US", "35124"),
new Location("LocC", "409 Gates St.", "Hightstown", "NJ", "", "08520"),
new Location("LocD", "540 Cemetery Street", "Brooklyn", "NY", "US", "11203"), destinationLocation })
.collect(Collectors.toList());
}
}
- Code for DeliveryInProgress class:
xxxxxxxxxx
package org.trishinfotech.state;
import java.util.ArrayList;
import java.util.List;
public class DeliveryInProgress implements ShipmentState {
protected List<Location> shipmentItinerary = new ArrayList<Location>();
public DeliveryInProgress(List<Location> itinerary) {
super();
this.shipmentItinerary.addAll(itinerary);
}
public void processShipment(ShipmentContext context) {
int currentLocationIndex = context.getCurrentLocationIndex();
Shipment shipment = context.getShipment();
System.out.printf(
"Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\nCurrent shipment location is '%s'\n",
shipment.getOrderNumber(), shipment.getOrderItem(), name(), shipment.getDeliveryAddress(),
shipmentItinerary.get(currentLocationIndex++));
System.out.println("--------------------------------------------------------------");
// since destination address will be part of OutForDelivery; We skip last
// location of itinerary
if (currentLocationIndex == (shipmentItinerary.size() - 1)) {
context.setCurrentState(new OutForDelivery());
} else {
context.setCurrentLocationIndex(currentLocationIndex);
}
}
public String name() {
return "Delivery In Progress";
}
}
- Code for OutForDelivery class:
x
package org.trishinfotech.state;
public class OutForDelivery implements ShipmentState {
public OutForDelivery() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
if (context.isCustomerAtLocation()) {
context.setCurrentState(new Delivered());
} else {
context.setCurrentState(new DeliveryAttempted("Customer not at Home!"));
}
}
public String name() {
return "Out For Delivery";
}
}
To demonstrate how we can have different states based on condition, I made customer not available for 1st time we do OutForDelivery. So, that will go to DeliveryAttempted status.
- Code for DeliveryAttempted class:
xxxxxxxxxx
package org.trishinfotech.state;
public class DeliveryAttempted implements ShipmentState {
protected String reasonForUndelivered;
public DeliveryAttempted(String reasonForUndelivered) {
super();
this.reasonForUndelivered = reasonForUndelivered;
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s (%s)'.\nDelivery Address is '%s'\n",
shipment.getOrderNumber(), shipment.getOrderItem(), name(), reasonForUndelivered,
shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
// setting flag to make the delivery at next attempt.
// we can set it logically as well instead of hard coding.
context.setCustomerAtLocation(true);
context.setCurrentState(new OutForDelivery());
}
public String getReasonForUndelivered() {
return reasonForUndelivered;
}
public void setReasonForUndelivered(String reasonForUndelivered) {
this.reasonForUndelivered = reasonForUndelivered;
}
public String name() {
return "Delivery Attempted";
}
}
From DeliveryAttempted it will again go for OutForDelivery status and this time I made customer available at home to ensure the delivery.
- Code for Delivered class:
xxxxxxxxxx
package org.trishinfotech.state;
public class Delivered implements ShipmentState {
protected String deliveryNote;
public Delivered() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
deliveryNote = "Order Package Handover to Customer";
System.out.printf("Order#'%s' for '%s' has status '%s (%s)'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), deliveryNote, shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
context.setCurrentState(new Received());
}
public String getDeliveryNote() {
return deliveryNote;
}
public void setDeliveryNote(String deliveryNote) {
this.deliveryNote = deliveryNote;
}
public String name() {
return "Delivered";
}
}
- Code for Received class (The status for acknowledgement provided from customer upon receiving of the ordered item):
xxxxxxxxxx
package org.trishinfotech.state;
public class Received implements ShipmentState {
public Received() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
// this is the end of the order processing.
// if we like to make it further to return the item we can uncomment the below line.
// context.setCurrentState(new Returned());
}
public String name() {
return "Received";
}
}
Below two shipment-status Exception (package lost/damage) and Returned (customer does not like), I didn't use in the example. But provided the code to cover commonly used shipment status. Please feel free to use in the use cases you have.
- Code for Exception class:
xxxxxxxxxx
package org.trishinfotech.state;
public class Exception implements ShipmentState {
protected String exceptionMsg;
public Exception(String exceptionMsg) {
super();
this.exceptionMsg = exceptionMsg;
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s (%s)'.\nDelivery Address is '%s'\n",
shipment.getOrderNumber(), shipment.getOrderItem(), name(), exceptionMsg,
shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
context.setCurrentState(new Returned());
}
public String getExceptionMsg() {
return exceptionMsg;
}
public void setExceptionMsg(String exceptionMsg) {
this.exceptionMsg = exceptionMsg;
}
public String name() {
return "Exception";
}
}
- Code for Returned class:
xxxxxxxxxx
package org.trishinfotech.state;
public class Returned implements ShipmentState {
public Returned() {
super();
}
public void processShipment(ShipmentContext context) {
Shipment shipment = context.getShipment();
System.out.printf("Order#'%s' for '%s' has status '%s'.\nDelivery Address is '%s'\n", shipment.getOrderNumber(),
shipment.getOrderItem(), name(), shipment.getDeliveryAddress());
System.out.println("--------------------------------------------------------------");
// If we like to ship replacement we can uncomment the below line and implement
// status for replacement.
// context.setCurrentState(...));
}
public String name() {
return "Returned";
}
}
Now, its time to write Main class to execute and test the output:
xxxxxxxxxx
package org.trishinfotech.state;
public class Main {
public static void main(String[] args) {
Shipment shipment = new Shipment("OD12345FLNY17", "Apple iPhone 11 Pro (Midnight Green, 256GB)",
new Location("LocE", "101 W. Sage Ave.", "Brooklyn", "NY", "US", "11234"));
ShipmentContext context = new ShipmentContext(shipment);
do {
context.processShipment();
} while (notReceived(context.getCurrentState()));
// shipment received. so, we need to process the received status to get
// acknowledgement from customer as 'item received'.
context.processShipment();
}
private static boolean notReceived(ShipmentState currentState) {
return !"Received".equalsIgnoreCase(currentState.name());
}
}
Below is the output of the program:
xxxxxxxxxx
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Order Placed'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Processing Stock'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Ready For Packing'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Ready To Deliver'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivery In Progress'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
Current shipment location is 'LocA (54 Essex Rd. Palm Bay FL US 32907)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivery In Progress'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
Current shipment location is 'LocB (335 Hall Street Pelham AL US 35124)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivery In Progress'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
Current shipment location is 'LocC (409 Gates St. Hightstown NJ 08520)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivery In Progress'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
Current shipment location is 'LocD (540 Cemetery Street Brooklyn NY US 11203)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Out For Delivery'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivery Attempted (Customer not at Home!)'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Out For Delivery'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Delivered (Order Package Handover to Customer)'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
Order#'OD12345FLNY17' for 'Apple iPhone 11 Pro (Midnight Green, 256GB)' has status 'Received'.
Delivery Address is 'LocE (101 W. Sage Ave. Brooklyn NY US 11234)'
--------------------------------------------------------------
I hope this tutorial helped and demonstrate the concept and implementation of the State Design Pattern.
Source Code can be found here: State-Design-Pattern-Sample-Code
Need more articles, please visit my profile: Brijesh Saxena
Liked the article? Please don't forget to press that like button. Happy coding!
Opinions expressed by DZone contributors are their own.
Comments