Skip to content

State

There is usually some configuration in complex applications. This configuration impacts the reference values in objects, and the specific field values of such objects can influence the behavior of the application when executing a certain process. The above description briefly represents the behavior of the State pattern. It describes a way to change the behavior of an object as a reaction to the same request, depending on its state.

Construction and Example

Let's assume that we are writing software for an automatic parking checkout where we can pay for parking with a credit card. The cash register has a message display, a card sensor and a button for printing a ticket. Pressing the button to print can be successful (print ticket) or not, depending on the current state such a cash register (e.g. no paper to print the ticket, no payment for parking or after paying the fee)

Without using the State pattern, the state would be managed inside the class representing the parking lot:

public enum MoneyMachineState {
  NO_PAPER,
  NEED_PAYMENT,
  PAID_READY_TO_PRINT,
  UNAVAILABLE
}
@Slf4j
public class ParkingTicketVendingMachine {

  private MoneyMachineState state = MoneyMachineState.NEED_PAYMENT;
  private int printingPaperPieces = 100;
  private String message;

  public void setMessage(final String message) {
    this.message = message;
  }

  public void setState(final MoneyMachineState state) {
    this.state = state;
  }

  public void addPrintingPaperPieces(final int pieces) {
    if (pieces <= 0) {
      throw new UnsupportedOperationException("Cannot add non positive number of pieces");
    }
    printingPaperPieces += pieces;

    if (state == MoneyMachineState.NO_PAPER) {
      state = MoneyMachineState.NEED_PAYMENT;
    }
    message = "Please pay for the parking";
  }

  public void payForOneHourWithCreditCard() {
    if (state == MoneyMachineState.NEED_PAYMENT) {
      log.info("Paying for parking 5$");
      state = MoneyMachineState.PAID_READY_TO_PRINT;
    }
    message = "Please click the button to print the ticket";
  }

  public void printTicket() {
    if (state == MoneyMachineState.PAID_READY_TO_PRINT) {
      printingPaperPieces -= 1;
      log.info("Ticket valid thru " + LocalDateTime.now().plusHours(1));

      if (printingPaperPieces == 0) {
        state = MoneyMachineState.NO_PAPER;
      } else {
        state = MoneyMachineState.NEED_PAYMENT;
      }
    }

    message = "Ticket printed. Please collect it";
  }

  public void goDown() {
    if (state == MoneyMachineState.PAID_READY_TO_PRINT) {
      log.info("Trying to revert last transaction");
    }
    state = MoneyMachineState.UNAVAILABLE;
    message = "Vending machine is unavailable. Try another one";
  }
}

The ParkingTicketVendingMachine class, despite a very simplified implementation and only four states, already has a lot of ins and outs. We can transfer this logic to classes that trigger it on the checkout based on the state. These classes also manage the state.

We make the following changes:

  • we create the ParkingTicketVendingMachineState interface that allows you to perform activities at the ticket machine based on its state
  • we add four implementations of this interface - they manage the machine and set the status accordingly
  • we are removing state shifting logic from the ParkingTicketVendingMachine class

With the State pattern, the implementation could look like this:

public enum MoneyMachineState {
  NO_PAPER,
  NEED_PAYMENT,
  PAID_READY_TO_PRINT,
  UNAVAILABLE
}
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;

@Slf4j
public class ParkingTicketVendingMachine {

  private MoneyMachineState state = MoneyMachineState.NEED_PAYMENT;
  private int printingPaperPieces = 100;
  private String message;

  public void setMessage(final String message) {
    this.message = message;
    System.out.println("MESSAGE: " + message);
  }

  public void setState(final MoneyMachineState state) {
    this.state = state;
  }

  public MoneyMachineState getState() {
    return state;
  }

  public int getPrintingPaperPieces() {
    return printingPaperPieces;
  }

  public void addPrintingPaperPieces(final int pieces) {
    if (pieces <= 0) {
      throw new UnsupportedOperationException("Cannot add non positive number of pieces");
    }
    printingPaperPieces += pieces;
    message = "Please pay for the parking";
  }

  public void payForOneHourWithCreditCard() {
    log.info("Paying for parking 5$");
    message = "Please click the button to print the ticket";
  }

  public void printTicket() {
    printingPaperPieces -= 1;
    log.info("Ticket valid thru " + LocalDateTime.now().plusHours(1));
    message = "Ticket printed. Please collect it";
  }

  public void goDown() {
    log.info("Trying to revert last transaction");
    message = "Vending machine is unavailable. Try another one";
  }

}
public interface ParkingTicketVendingMachineState {
  void moveCreditCardToSensor();
  void pressPrintingButton();
  void openMachineAndAddPrintingPaperPieces();
}
public class NoPrintingPaperState implements ParkingTicketVendingMachineState {

  private final ParkingTicketVendingMachine machine;

  public NoPrintingPaperState(final ParkingTicketVendingMachine machine) {
    this.machine = machine;
  }

  @Override
  public void moveCreditCardToSensor() {
    machine.setMessage("Cannot pay because there is no printing paper");
  }

  @Override
  public void pressPrintingButton() {
    machine.setMessage("Please call 728 123 1234 for additional printing paper");
  }

  @Override
  public void openMachineAndAddPrintingPaperPieces() {
    machine.addPrintingPaperPieces(100);
    machine.setState(MoneyMachineState.NEED_PAYMENT);
  }
}
public class PaidState implements ParkingTicketVendingMachineState {

  private final ParkingTicketVendingMachine machine;

  public PaidState(final ParkingTicketVendingMachine machine) {
    this.machine = machine;
  }

  @Override
  public void moveCreditCardToSensor() {
    machine.setMessage("Already paid. Please press button for printout");
  }

  @Override
  public void pressPrintingButton() {
    machine.printTicket();
    if (machine.getPrintingPaperPieces() == 0) {
      machine.setState(MoneyMachineState.NO_PAPER);
    } else {
      machine.setState(MoneyMachineState.NEED_PAYMENT);
    }
  }

  @Override
  public void openMachineAndAddPrintingPaperPieces() {
    machine.setMessage("Only authorized personel can add paper");
  }
}
public class StillNeedToPayState implements ParkingTicketVendingMachineState {

  private final ParkingTicketVendingMachine machine;

  public StillNeedToPayState(final ParkingTicketVendingMachine machine) {
    this.machine = machine;
  }

  @Override
  public void moveCreditCardToSensor() {
    machine.payForOneHourWithCreditCard();
    if (machine.getState() == MoneyMachineState.NEED_PAYMENT) {
      machine.setState(MoneyMachineState.PAID_READY_TO_PRINT);
    }
  }

  @Override
  public void pressPrintingButton() {
    machine.setMessage("You to pay first");
  }

  @Override
  public void openMachineAndAddPrintingPaperPieces() {
    machine.setMessage("Only authorized personel can add paper");
  }
}
public class UnavailableState implements ParkingTicketVendingMachineState {

  private final ParkingTicketVendingMachine machine;

  public UnavailableState(final ParkingTicketVendingMachine machine) {
    this.machine = machine;
  }

  @Override
  public void moveCreditCardToSensor() {
    machine.setMessage("Vending machine is unavailable");
  }

  @Override
  public void pressPrintingButton() {
    machine.goDown();
    machine.setState(MoneyMachineState.UNAVAILABLE);
  }

  @Override
  public void openMachineAndAddPrintingPaperPieces() {
    machine.setMessage("Vending machine is unavailable");
  }
}
public class StateUsage {
  public static void main(String[] args) {
    final ParkingTicketVendingMachine machine = new ParkingTicketVendingMachine();
    ParkingTicketVendingMachineState state = new StillNeedToPayState(machine);
    state.openMachineAndAddPrintingPaperPieces();
    state.pressPrintingButton();
    state.moveCreditCardToSensor();

    state = new PaidState(machine);
    state.moveCreditCardToSensor();
    state.openMachineAndAddPrintingPaperPieces();
    state.pressPrintingButton();
  }
}

Using the State pattern, the number of classes defined in the application can increase significantly, but finally we gain readability. If your application has many possible states that change frequently, it makes sense to use the State pattern. For this purpose you can use a ready-made solution, e.g. spring-statemachine.