Saturday, February 4, 2023

How to use State Design Pattern in Java? Vending Machine Example

The State design pattern is a behavioral pattern, first introduced by GOF in their class is book design patterns. State pattern looks similar to Strategy pattern but it helps in managing object state and thus enabling them to behave differently in a different state. In this example, we will take a famous object-oriented design interview question, implementing Vending Machine in Java. In the past, we have solved this problem without using any design pattern, but here we will use state design patterns to create a vending machine with different states.  This example will not just help you to understand how to implement State Design Pattern in Java but also gives you experience about when to use State Pattern on your application. 

If you know how vending machine works, you can divide its operation into mainly four states sold-out, idle, processing, and sold. Sold-out when the vending machine is just started and not initialized, or all items have already sold. 

Idle, when the vending machine is waiting for the customer to select an item, Processing, once the customer has selected item and started inserting a coin, Sold when the customer has paid the amount.

We can use a State design pattern to model these states of vending machines. In this example, we have an abstract class called State to represent the state of Vending Machine, which provides a default implementation of various methods, which is called by Context (Vending Machine in this case), each of our State, like Idle, Processing, Sold, SoldOut then extends this abstract class and overrides the method, which can be called in those state.

All other methods, which is not supposed to be called on those state, will do default operation, which can be either do nothing or throw an exception, as defined in State abstract class. Each State class keeps a reference to Context, through which they are associated and they also make state transition, I mean to change the current state of Vending Machine. 



How to Design Vending Machine in Java using State Design Pattern - Example

Here is our complete code example of implementing Vending Machine using State Design patterns in Java. When Vending Machine starts, it's initially on the SoldOut state then it moves to Idle, once it's initialized with a default number of Items and default number of coins.

How to use State Design Pattern in Java



Vending Machine also provides a method that is present in State abstract class like select(Item i), insert(Coin c), refund(), etc, but they are delegated to the current state. When customers select products by calling select(Item i) on Vending Machine, it delegates to currentState.select(i), which is valid if Machine is in the Idle state and then it will move to Processing but will throw IllegalStateException if called on other states.

By the way, I haven't gone into the technical definition of State Design Pattern but if you are curious, you can always see these best Java design pattern courses to learn all 24 GOF design Patterns and their modern implementations in Java.

How to use State Design Pattern in Java? Vending Machine Example

Now, let's see the code, its quite similar to our Vending Machine Problem Code but this time, I have used State Pattern to solve the problem. 

Files:
  • Coin.java
  • Idle.java
  • inventory.java
  • Item.java
  • NotSufficientChangeException.java
  • Processing.java
  • Sold.java
  • SoldOut.java
  • State.java
  • VendingMachine.java
  • VendingMachineTest.java

Coin.java
==========

public enum Coin {
    PENNY(1), NICKLE(5), DIME(10), QUARTER(25);

    private int value;
    private Coin(int value){
        this.value = value;
    }

    public int value(){
        return value;
    }
}

State.java
============
import java.util.List;

public class State {

    public void insert(Coin c){
        throw new IllegalStateException();
    }

    public List refund(){
         throw new IllegalStateException();
    }

    public int choose(Item i){
         throw new IllegalStateException();
    }

    public Item dispense(){
         throw new IllegalStateException();
    }

    public List getChange() {
        throw new IllegalStateException();
    }
}

Idle.java
==========

public class Idle extends State{

    private VendingMachine machine;

    public Idle(VendingMachine machine){
        this.machine = machine;
    }

    @Override
    public int choose(Item i) {
        if(machine.itemInvertory.getCount(i) >= 1){
            machine.currentItem = i;
            machine.setState(new Processing(machine));
        }else{
            System.out.println(i + " sold out, Please try another drink");
        }
        return i.getPrice();
    }

}

Sold.java
==========
import java.util.List;

public class Sold extends State{
    private VendingMachine machine;

    public Sold(VendingMachine machine){
        this.machine = machine;
    }

    @Override
    public Item dispense(){
        if(machine.itemInvertory.isEmpty()){
            machine.setState(new SoldOut(machine));
        }
        machine.balance = machine.balance  - machine.currentItem.getPrice();
 
        machine.itemInvertory.take(machine.currentItem);
 
        Item sold = machine.currentItem;
        machine.currentItem = null;
 
        return sold;
    }

    @Override
    public List getChange(){
        List change = machine.getChange(machine.balance);
        return change;
    }
}

SoldOut.java
==============

public class SoldOut extends State{

    private VendingMachine machine;

    public SoldOut(VendingMachine machine){
        this.machine = machine;
    }

}


Inventory.java
===============
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class Inventory {
    private Map store = new ConcurrentHashMap();

    public int getCount(I item){
        Integer count = store.get(item);
        return count != null ? count : 0;
    }

    public void add(I item){
        int count = getCount(item);
        store.put(item, ++count);
    }

    public void take(I item){
        int count = getCount(item);
        store.put(item, --count);
    }

    public boolean isEmpty(){
        return store.isEmpty();
    }

    boolean has(I i) {
        return getCount(i) > 0;
    }
}

Item.java
==========

public enum Item {
    COKE(70), PEPSI(80), SPRITE(90);

    private int price;

    private Item(int price){
        this.price = price;
    }

    public int getPrice(){
        return price;
    }
}

NotSufficientChangeException.java
==================================

public class NotSufficientChangeException extends RuntimeException {

    private String message;

    public NotSufficientChangeException(String string) {
        this.message = string;
    }

    @Override
    public String getMessage() {
        return message;
    }

}

Processing.java
================
import java.util.List;

public class Processing extends State{

    private VendingMachine machine;

    public Processing(VendingMachine machine){
        this.machine = machine;
    }

    @Override
    public void insert(Coin c) {
        machine.coinInvertory.add(c);
        machine.balance = machine.balance + c.value();
        if (machine.balance >= machine.currentItem.getPrice()) {
     
            if (machine.hasChange(machine.balance - machine.currentItem.getPrice())) {
                machine.setState(new Sold(machine));
            } else {
                System.out.println("Machine don't have 
                  sufficient change, Please take refund");
            }
        }

    }


    @Override
    public List refund() {
        machine.currentItem = null;
        machine.setState(new Idle(machine));
        List change = machine.getChange(machine.balance);
        machine.balance = 0;
        return change;
    }

}



VendingMachine.java
=====================
import java.util.ArrayList;
import java.util.List;


public class VendingMachine {
    private State state;
    Inventory itemInvertory = new Inventory();
    Inventory coinInvertory = new Inventory();

    Item currentItem;
    int balance;

    public VendingMachine(){
        state = new SoldOut(this);
        initialize();
    }

    public void insert(Coin c){
        state.insert(c);
    }
    public List refund(){
        return state.refund();
    }
    public int choose(Item i){
       return state.choose(i);
    }

    public Item dispense(){
        return state.dispense();
    }

    public void setState(State newState){
        state = newState;
    }

    public List getChange(){
        return state.getChange();
    }

    private void initialize() {
       loadCoins();
       loadItems();
       this.state = new Idle(this);
    }

    private void loadCoins(){
        for(Coin c: Coin.values()){
           coinInvertory.add(c);
           coinInvertory.add(c);
           coinInvertory.add(c);
           coinInvertory.add(c);
           coinInvertory.add(c);
       }
    }

    private void loadItems(){
        for(Item i: Item.values()){
           itemInvertory.add(i);
           itemInvertory.add(i);
           itemInvertory.add(i);
           itemInvertory.add(i);
           itemInvertory.add(i);
       }
    }

    List getChange(int balance) {
        List change = new ArrayList();
        while(balance != 0){
            if(balance >= Coin.QUARTER.value() && coinInvertory.has(Coin.QUARTER)){
                balance -= Coin.QUARTER.value();
                change.add(Coin.QUARTER); coinInvertory.take(Coin.QUARTER);
         
            }else if(balance >= Coin.DIME.value() && coinInvertory.has(Coin.DIME) ) {
                balance -= Coin.DIME.value();
                change.add(Coin.DIME); coinInvertory.take(Coin.DIME);
         
            }else if(balance >= Coin.NICKLE.value() && coinInvertory.has(Coin.NICKLE)){
                balance -= Coin.NICKLE.value();
                change.add(Coin.NICKLE); coinInvertory.take(Coin.NICKLE);
         
            }else if(balance >= Coin.PENNY.value() && coinInvertory.has(Coin.PENNY)) {
                balance -= Coin.PENNY.value();
                change.add(Coin.PENNY); coinInvertory.take(Coin.PENNY);
            }
     
           if(coinInvertory.isEmpty() && balance >0){
               throw new NotSufficientChangeException("Not Sufficient 
                        Change for this purchase");
           }
        }
 
        return change;
    }

    boolean hasChange(int change) {
        try{
           List coins = getChange(change);
     
           //returning coins back to inventory
           for(Coin c : coins){
               coinInvertory.add(c);
           }
     
        }catch(NotSufficientChangeException ex){
            return false;
        }  
        return true;
    }

}



VendingMachineTest.java

Here is our test program to test the code of VendingMachine which we have implemented using a State design pattern. These tests will confirm that whether state transitions work as expected or not. The code below uses JUnit annotation hence you need JUnit 4.0 or JUnit 5.0 framework JAR files in your classpath. I have a couple of tests to buy drinks with exact change and with more change to see if our machine is working properly or not.

For your practice, I have also added a couple of blank test methods like buyMultipleDrinks(), refund(), and buyAllDrinks(), you can implement those methods to get some practice on writing JUnit test cases

This UML diagram of State Design Pattern will also help you understand class structure and their relationship. 


State Design Pattern Example - Vending Machine in Java

.

import java.util.List;
import org.junit.Test;
import static org.junit.Assert.;


public class VendingMachineTest {
    private VendingMachine machine = new VendingMachine();

    public VendingMachineTest(){
        System.out.println("JUnit Framework calls 
           Constructor of test class before executing test methods");
    }

    @Test
    public void buyDrinkWithExactAmount(){
        int price = machine.choose(Item.COKE);
        assertEquals(70, price);
        assertEquals(Item.COKE, machine.currentItem);
 
        machine.insert(Coin.QUARTER);
        machine.insert(Coin.QUARTER);
        machine.insert(Coin.DIME);
        machine.insert(Coin.DIME);
        assertEquals(70, machine.balance);
        assertEquals(7, (int) machine.coinInvertory.getCount(Coin.DIME));
        assertEquals(7, (int) machine.coinInvertory.getCount(Coin.QUARTER));
 
        Item i = machine.dispense();
        assertEquals(Item.COKE, i);
        assertEquals(4, (int) machine.itemInvertory.getCount(i));
 
        List change = machine.getChange();
        assertTrue(change.isEmpty());
 
 
    }

    @Test
    public void buyDrinkWithMoreAmount(){
        int price = machine.choose(Item.SPRITE);
        assertEquals(90, price);
        assertEquals(Item.SPRITE, machine.currentItem);
 
        machine.insert(Coin.QUARTER);
        machine.insert(Coin.QUARTER);
        machine.insert(Coin.QUARTER);
        machine.insert(Coin.QUARTER);
 
        assertEquals(100, machine.balance);
        assertEquals(9, (int) machine.coinInvertory.getCount(Coin.QUARTER));
 
        Item i = machine.dispense();
        assertEquals(Item.SPRITE, i);
        assertEquals(4, machine.itemInvertory.getCount(i));
        assertEquals(5, machine.itemInvertory.getCount(Item.COKE));
 
        List change = machine.getChange();
        assertEquals(1, change.size());
        assertEquals(Coin.DIME, change.get(0));
        assertEquals(4, machine.coinInvertory.getCount(Coin.DIME));
         
    }

    @Test
    public void buyMultipleDrinks(){
        //TODO
    }

    @Test
    public void refund(){
        //TODO
    }

    @Test
    public void buyAllDrinks(){
        //TODO
    }
}


That's all on How to use State design patterns to implement a Vending Machine in Java. You can see a clear advantage in terms of managing logic to change states and implementing the different behavior in different states compared to our previous example. I highly recommend you implement Vending Machine or any state-oriented problem using State design patterns in Java. It results in both simpler and efficient solutions. 

Other Object-Oriented Design Pattern Tutorials from Javarevisited
  • 18 OOP Design Pattern Interview Questions for experienced Programmers (list)
  • 7 Best Java Design Pattern courses for Beginners (courses)
  • 20 Software design Questions from Programming Interviews (list)
  • How to implement Builder design pattern in Java? (tutorial)
  • Difference between Factory and Abstract Factory Pattern? (example)
  • How to implement Adapter pattern in Java (adapter pattern)
  • 10 OOP Design Principle Every Java developer should learn (solid principle)
  • How to implement Composite Pattern in Java? (composite pattern example)
  • 7 Best Books to learn the Design Pattern in Java? (books)
  • How to implement the Strategy Design Pattern in Java? (example)
  • 20 System Design Interview Questions (list)
  • How to implement Command Pattern in Java (command pattern)
  • How to implement a Decorator design pattern in Java? (tutorial)
  • How to use Factory method design pattern in Java? (tutorial)
  • What is the difference between Factory pattern and Dependency Injection in Java? (answer)
  • 5 Books to learn Object-Oriented Design Patterns in Java? (books)

Thanks for reading this tutorial, if you like this content then please share it with your friends and colleagues. If you have any feedback or suggestion then please drop a comment. If you just want to do one thing at this moment then go and read "Head First Design Pattern"

P. S. - If you are keen to learn Design patterns in Java but looking for a free online training course to start with then you can also check out these Java design pattern courses for experienced developers. It contains my favorite courses to learn design patterns in depth. 

1 comment :

financial engineer said...

don't forget the Inventory generic parameter:
public class Inventory<I>
and VendingMachine generic type fields:
Inventory<Item> itemInvertory = new Inventory();
Inventory<Coin> coinInvertory = new Inventory();
otherwise nice

Post a Comment