State Design Pattern

For an object with multiple states, each state has it’s own specific behavior.

The state pattern just plugs in the required state behavior; as the object moves from one state to the other.

This is a behavioral design pattern that helps us design the state lifecycles in a clean and flexible way.

Its very similar to the strategy pattern as we build the lifecycle features using composition. For each state in the lifecycle, it allows us to group the related actions as pluggable components. When the object moves between lifecycle states, the pattern helps us plug in the appropriate state behavior of the object.

To make it clear, let us look at it with an example.

 

How does the pattern work?

State Requirements

Consider a simple document lifecycle. Lets say it goes from draft, review to approved state as shown.

Design using State Pattern

As shown above, the document has 3 states and 5 state dependent features.

One way to implement these state specific operations is to check for the current state within the operations as shown below in option-1.

State Design Pattern

The option-2 shows how can we design this using state pattern.

  • Instead of conditional checks for the states as in option-1, we can group the state specific actions for these operations in a separate class. For example, DraftState will have all actions related to draft state.
  • Then, instead of the state as a status, we can inject these state specific behavior so that the operations behave as per the injected state.

For example, while in draft state we can inject the DraftState as the state. As shown under option-2.x, the edit() operation in DraftState will allow us to open the document in edit mode. However, the same edit() will throw exception when we move to any other state.

What advantages do we get ?
  • When we have a number of states, we can manage the state specific behaviors separately, in a cleaner way.
  • Addition, removal or replacements of these state behaviors is much easier, as we only have to touch the state specific files those change.
  • It will help us the do away with the conditional cases distributed across the features for our multiple states.

 

Demo Implementation

The code below shows what we have discussed so far.

package spectutz.dp.behavior.state.doc;
import spectutz.dp.behavior.state.doc.state.DraftState;
import spectutz.dp.behavior.state.doc.state.IState;

public class Document {
	private IState state;
	private String name;
	
	
	public Document(String name) {
		this.name =name;
		System.out.println("\nDocument created in : Draft state.");

		this.state= new DraftState();
	}
	public void setState(IState state) {
		System.out.println("\nDocument state set to : "+state.getClass().getSimpleName());
		this.state = state;
	}
	
	public void open() {
		this.state.open(this);	
	}
	public void edit() {
		this.state.edit(this);	
	}
	public void submitForReview() {
		this.state.submitForReview(this);	
	}
	public void addReview() {
		this.state.addReview(this);	
	}
	public void submitApproval(boolean isApproved) {
		this.state.submitApproval(this, isApproved);	
	}
	
}
package spectutz.dp.behavior.state.doc.state;
import spectutz.dp.behavior.state.doc.Document;

public interface IState {
	
	public void open(Document doc); 
	
	//Allowed only in draft state
	public void edit(Document doc); 
	public void submitForReview(Document doc); 
	
	//Allowed only in review state
	public void addReview(Document doc);
	public void submitApproval(Document doc,boolean isApproved);
	
}
package spectutz.dp.behavior.state.doc.state;
import spectutz.dp.behavior.state.doc.Document;

public  abstract class AbstractState implements IState{

	
	public void open(Document doc) {
		System.out.println("Document opened in read-only mode.");		
	}
	
	public void edit(Document doc) {
		System.out.println("Sorry, the current state does not allow edit.");		
	}

	public void submitForReview(Document doc){
		System.out.println("Sorry, the current state does not allow submitting for review.");		
	}
	
	public void addReview(Document doc){
		System.out.println("Sorry, the current state does not allow adding review.");		
	}

	public void submitApproval(Document doc, boolean isApproved) {
		System.out.println("Sorry, the current state does not accept approval.");
	}


	//Note : 
	// 1. We can add appropriate business exceptions where we do not allow an operation.
	//  Just to keep it simple we have used sops instead.
	// 2. Other conditions like document owner, reviewer checks has been omitted for brevity.
	
	// The goal here is to have separate implementation class for each state, 
	// this abstract class being the place for default implementations.  
}

package spectutz.dp.behavior.state.doc.state;
import spectutz.dp.behavior.state.doc.Document;

public class DraftState extends AbstractState{

	@Override
	public void edit(Document doc) {
		System.out.println("Document opened in edit mode.");		
	}

	@Override
	public void submitForReview(Document doc){
		doc.setState(new ReviewState());
		System.out.println("Document submitted for review.");		
	}
}

package spectutz.dp.behavior.state.doc.state;
import spectutz.dp.behavior.state.doc.Document;

class ReviewState extends AbstractState{
	
	@Override
	public void addReview(Document doc){
		System.out.println("Review added successfully.");		
	}

	@Override
	public void submitApproval(Document doc, boolean isApproved) {
		if(isApproved) {
			doc.setState(new ApprovedState());
			System.out.println("Approved: Document state changed to Approved mode.");
		}else {
			doc.setState(new DraftState());
			System.out.println("Send for Rework: Document state changed to Draft mode.");			
		}
	}
}

package spectutz.dp.behavior.state.doc.state;

class ApprovedState extends AbstractState{
// Uses the default implementations
}

When the document moves from one state to the other, the implementation assigns the corresponding state behavior to its state variable.

Hence, as we can see in the output, the same operations behaves differently as per the state table given above.

package spectutz.dp.behavior.state;
import spectutz.dp.behavior.state.doc.Document;

public class DocumentSateDemo{
	public static void main(String[] args) {		
		Document requiremetsDoc = new Document("StarWarGame");
		
		
		requiremetsDoc.edit();
		requiremetsDoc.submitApproval(true);

		requiremetsDoc.submitForReview();		
		requiremetsDoc.addReview();
		requiremetsDoc.edit();

		requiremetsDoc.submitApproval(false);		
		requiremetsDoc.edit();

		requiremetsDoc.submitForReview();
		requiremetsDoc.addReview();

		requiremetsDoc.submitApproval(true);
		requiremetsDoc.edit();
		
		
	}
}
// ----Console output---- //
/*
Document created in : Draft state.
Document opened in edit mode.
Sorry, the current state does not accept approval.

Document state set to : ReviewState
Document submitted for review.
Review added successfully.
Sorry, the current state does not allow edit.

Document state set to : DraftState
Send for Rework: Document state changed to Draft mode.
Document opened in edit mode.

Document state set to : ReviewState
Document submitted for review.
Review added successfully.

Document state set to : ApprovedState
Approved: Document state changed to Approved mode.
Sorry, the current state does not allow edit.
*/

Conclusion

The state pattern is very similar to the strategy pattern where we build pluggable features. The state patterns additionally manage the plugging in of the new state behavior as the object goes through its state change.

Since it allows us to define each state behavior separately, it keeps our lifecycle management simple and flexible.