Sunday, March 6, 2016

The State design pattern

I love it when my next post subject comes from a 'real' life situation I am in. Recently I was working on a piece of code in which there was a class with a huge method. In this method there was a long switch block and based on the value of a property in the class, it performed some logic. This code was extended and edited by multiple developers over time. This is a common  code smell and a clear refactoring case, so I was a bit surprised that none took the time to improve the code by using the state design pattern. And just like that, I had the theme and motivation for my next design pattern tutorial!

The state design pattern is of the behavioral family and allows objects to alter their behavior depending on their internal state. For example, people should not talk much to me in the morning before I have a coffee! The state pattern is ideal for cases where the state of objects can change at runtime and the behavior for each state has to change as well. Many times, as was the case I saw, new states and behaviors are added and this requires changes in the class which violates the open/closed principle. Using the state pattern, we can remove long and difficult to follow switch blocks and have a clear, modularized and extendable piece of code.

To showcase how the state design pattern can be used, lets consider a coffee machine. Our coffee machine makes great coffee but it only does this when it has coffee and it has water. Based on its state, meaning it has water and it has coffee will either make us a cup of coffee or guide us to take it to the right state. The UML diagram of the components that we will build is presented below:


We start by creating the State inteface, which defines the behaviors that will be adjusted based on the state of the object. Our coffee machine can insert coffee, insert water and make coffee.

package com.tasosmartidis.design_patterns_tutorial.state;

public interface State {

    public void insertCoffee();
    
    public void insertWater();
    
    public void makeCoffee();
}

Next, we will define the different states a coffee machine can be in. It can be empty, which means that it contains neither water, nor coffee and thus it cannot make coffee. In cases that someone inserts coffee, then it will print the appropriate message and  change the state of the coffee machine to 'has coffee'. Similar for inserting water.

package com.tasosmartidis.design_patterns_tutorial.state;

public class IsEmpty implements State {

    private CoffeeMachine coffeeMachine;

    public IsEmpty(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }
    
    public void insertCoffee() {
        System.out.println("Inserting coffee..."); 
        coffeeMachine.setMachineState(coffeeMachine.getHasCoffeeState());
    }
 
    public void insertWater() {
        System.out.println("Inserting water..."); 
        coffeeMachine.setMachineState(coffeeMachine.getHasWaterState());
    }
 
    public void makeCoffee() {
        System.out.println("Cannot make coffee, there is no water or coffee!");     
    }
}

So lets now create a state where coffee is already inserted. It still cannot make coffee and will not accept more coffee since it has already. But it will certainly accept water to be inserted, print appropriate message and set the state to has water and coffee since then it will have both.
package com.tasosmartidis.design_patterns_tutorial.state;

public class HasCoffee implements State{
    
    private CoffeeMachine coffeeMachine;

    public HasCoffee(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    public void insertCoffee() {
        System.out.println("Cannot insert coffee, it is already full!");         
    }
 
    public void insertWater() {
        System.out.println("Inserting water...");
        coffeeMachine.setMachineState(coffeeMachine.getHasCoffeeAndWaterState());
    }
 
    public void makeCoffee() {
        System.out.println("Cannot make coffee, it has coffee but no water!");      
    }
}

In the case that water has been inserted to the coffee machine, its state change to 'has water'.
package com.tasosmartidis.design_patterns_tutorial.state;

public class HasWater implements State {
     
    private CoffeeMachine coffeeMachine;

    public HasWater(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }
    
    public void insertCoffee() {
        System.out.println("Inserting coffee..."); 
        coffeeMachine.setMachineState(coffeeMachine.getHasCoffeeAndWaterState());
    }
 
    public void insertWater() {
        System.out.println("Cannot insert water, it is already full!");
    }
 
    public void makeCoffee() {
        System.out.println("Cannot make coffee, it only has water!");       
    }

}

Of course inserting water does not always change state to 'has water'. The change of the state depends also in the current state of the machine. For example, when a machine is in state 'has water' and coffee is inserted, its state changes to 'has coffee and water'. Let's define this state:
package com.tasosmartidis.design_patterns_tutorial.state;

public class HasCoffeeAndWater implements State {

    private CoffeeMachine coffeeMachine;

    public HasCoffeeAndWater(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    public void insertCoffee() {
        System.out.println("Cannot insert coffee, it is already full!");         
    }
 
    public void insertWater() {
        System.out.println("Cannot insert coffee, it is already full!");    
    }
 
    public void makeCoffee() {
        System.out.println("Making coffee...");
        coffeeMachine.setMachineState(coffeeMachine.getMakingCoffeeState());
    }
}

Finally, when the machine has both coffee and water, it is ready to make us a nice cup. This is an additional state, and while it is making coffee, it cannot perform anything else, like adding coffee or water. When making coffee is finished, its state returns back to 'is empty'.
package com.tasosmartidis.design_patterns_tutorial.state;

public class MakingCoffee implements State {
     
    private CoffeeMachine coffeeMachine;

    public MakingCoffee(CoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;   
    }
    
    public void insertCoffee() {
        System.out.println("Cannot insert coffee while making coffe!"); 
    }
 
    public void insertWater() {
        System.out.println("Cannot insert water while making coffee!"); 
    }
 
    public void makeCoffee() {
        System.out.println("Cannot make coffee its busy!"); 
        coffeeMachine.setMachineState(coffeeMachine.getIsEmptyState());         
    }

}

Great! So now all the possible states of our coffee machine and their behavior are defined. Next we will define the coffee machine. Our coffee machine will have its current state, which we can set and the behaviors which change based on the state, for inserting coffee, water and making coffee. I also define all possible states in the object with getters, which they will help moving the current state of the machine to the desired one based on thegiven action. Basicallly you can see what I mean if you look at the makeCoffeemethod of the HasCoffeeAndWater class. When the machine starts making coffee, it prints the appropriate message and sets the current state of the machine, using the getter of the machine for the state we want to set it to. An alternative would be to create a new state object in the set method, e.g., "coffeeMachine.setMachineState(new MakingCoffeeState())", but I do not want to create a new object every time the state changes. The coffee machine source is shown below:
package com.tasosmartidis.design_patterns_tutorial.state;

public class CoffeeMachine {

    private State machineState;
    private State isEmpty;
    private State hasCoffee;
    private State hasWater;
    private State hasCoffeeAndWater;
    private State makingCoffee; 
    
    
    public CoffeeMachine() {
        this.machineState = new IsEmpty(this);
        
        this.isEmpty = new IsEmpty(this);
        this.hasCoffee = new HasCoffee(this);
        this.hasWater = new HasWater(this);
        this.hasCoffeeAndWater = new HasCoffeeAndWater(this);
        this.makingCoffee = new MakingCoffee(this);
    }
    
    public void setMachineState(State newState) {
        this.machineState = newState;
    }
    
    public void insertCoffee() {
        machineState.insertCoffee(); 
    }
 
    public void insertWater() {
        machineState.insertWater();
    }

    public void makeCoffee() {
        machineState.makeCoffee();
        
    }
    
    public State getIsEmptyState() { return isEmpty; }
    public State getHasCoffeeState() { return hasCoffee; }
    public State getHasWaterState() { return hasWater; }
    public State getHasCoffeeAndWaterState() { return hasCoffeeAndWater; }


Now let's see our coffee machine in action! We will test how the coffee machine works based on its state and the actions requested from the machine.
package com.tasosmartidis.design_patterns_tutorial.state;

public class CoffeeMachineDemo {

    public static void main(String[] args) {
        
        CoffeeMachine coffeeMachine = new CoffeeMachine();

        coffeeMachine.makeCoffee();
        coffeeMachine.insertCoffee();
        coffeeMachine.makeCoffee();
        coffeeMachine.insertWater();
        coffeeMachine.makeCoffee();
        coffeeMachine.makeCoffee();
        coffeeMachine.makeCoffee();     
    }
}

The main method above, has some simple commands and flow. Let's break down what we expect:

  1. A new coffee machine object is created - its state is empty
  2. Ask to make coffee - it shouldn't be able to
  3. We insert coffee - change state to has coffee
  4. Ask to make coffee - still water is missing
  5. We insert water - now machine has both coffee and water
  6. Ask to make coffee - makes us coffee and change state to making coffee
  7. Ask to make coffee again - it cannot, it is busy making coffee and changes state to empty
  8. Ask to make coffe last time - it cannot, machine is empty
And when we run the program, we get the following output:
















It worked as expected! So this brief tutorial presented the state design pattern and when it can be useful. It should be stressed however, that the state pattern can get quite complicated. The states used above are not many, but if we kept extending with more states and behaviors there is complexity from having to keep track of how to move from one state to the other and how to behave in each state. It is a useful design but must be applied with care. 

As always, the code is available in github.

2 comments:

  1. In all State concrete classes you've used CoffeeMachine as agregation, received in constructor.
    The State Interface should be changed to abstract in the diagram, with this field, since it is shared by all implementations, and added a bidirectional association.

    ReplyDelete
    Replies
    1. Thanks for your comment! It is a good idea and you could do that.

      Delete