ExerciseDifficulty: Advanced2 min

Practice: A Vending Machine

Idle, has coin and dispensing, with insert, select and refund.

A vending machine is a natural State: the same buttons do different things depending on whether a coin is in. This exercise asks you to draw its states, write them, and say which class decides each transition. Work out your answer before opening the solution.

The machine has three buttons: insert a coin, select an item, and refund. It holds one coin at a time.

  • With no coin, selecting asks for a coin, and refund has nothing to return.
  • With a coin, a second coin is returned, refund returns the coin, and selecting dispenses the item.
  • After dispensing, the machine is ready for the next customer.
  1. Draw the state diagram.
  2. Write the states and the machine in Python.
  3. For each transition, say which class sets it.
Show a solution
The vending machine's states
py
from abc import ABC


class VendingState(ABC):
    def insert_coin(self, machine: "VendingMachine") -> None:
        print("  coin returned: not accepted now")

    def select(self, machine: "VendingMachine") -> None:
        print("  nothing happens")

    def refund(self, machine: "VendingMachine") -> None:
        print("  no coin to refund")


class Idle(VendingState):
    def insert_coin(self, machine: "VendingMachine") -> None:
        print("  coin accepted")
        machine.state = HasCoin()

    def select(self, machine: "VendingMachine") -> None:
        print("  insert a coin first")


class HasCoin(VendingState):
    def select(self, machine: "VendingMachine") -> None:
        print("  dispensing")
        machine.state = Dispensing()
        machine.state.finish(machine)

    def refund(self, machine: "VendingMachine") -> None:
        print("  coin refunded")
        machine.state = Idle()


class Dispensing(VendingState):
    def finish(self, machine: "VendingMachine") -> None:
        print("  item dropped")
        machine.state = Idle()


class VendingMachine:
    def __init__(self) -> None:
        self.state: VendingState = Idle()

    def insert_coin(self) -> None:
        self.state.insert_coin(self)

    def select(self) -> None:
        self.state.select(self)

    def refund(self) -> None:
        self.state.refund(self)


machine = VendingMachine()
for action in ("select", "insert_coin", "insert_coin", "refund", "insert_coin", "select", "refund"):
    print(f"{action} in {type(machine.state).__name__}:")
    getattr(machine, action)()
OutputRan with python:3.12-slim (Python 3.12.13)
select in Idle:
  insert a coin first
insert_coin in Idle:
  coin accepted
insert_coin in HasCoin:
  coin returned: not accepted now
refund in HasCoin:
  coin refunded
insert_coin in Idle:
  coin accepted
select in HasCoin:
  dispensing
  item dropped
refund in Idle:
  no coin to refund
Each request the diagram has no arrow for falls through to the base class's default.

Each transition is set by the state it leaves: Idle moves to HasCoin on a coin, HasCoin moves to Idle on a refund or to Dispensing on a selection, and Dispensing moves back to Idle when the item drops. The machine never sets its own state.

The base class VendingState holds the replies for requests with no arrow, so a second coin in HasCoin and a refund in Idle need no code in those classes. Dispensing has an extra method, finish, because leaving it is triggered by the item dropping rather than by a button.