VendingMachine ๐ค: The main controller object that manages the machine's overall state, user interactions, and internal components.
Item ๐ซ: A data object representing a single product with a code, name, and price.
Inventory ๐ฆ: Manages the collection of all items and tracks their current stock levels.
Coin ๐ช: An Enum that defines the valid types of currency the machine accepts and their monetary value (e.g., QUARTER = 25).
State โ๏ธ: An object that defines the machine's current behavior (e.g., IdleState, HasMoneyState) and determines which user actions are valid at any given moment.
The VendingMachine acts as the central hub, delegating all user actions to its current State object. โ๏ธ
The State contains the system's logic. It makes decisions by querying the Inventory for item details (like price and stock) and then instructs the VendingMachine on how to proceed. These instructions include updating the user's balance, dispensing an item, or transitioning the machine to a new State (e.g., from IdleState to HasMoneyState).
In essence, the VendingMachine is the body, the Inventory is the pantry, and the State is the brain directing the body's actions. ๐ค
In my design, I established an inheritance hierarchy primarily for the State objects, as this is the most effective way to manage the machine's behavior. This approach follows the classic State Design Pattern. โ๏ธ
I designed an abstract parent class, VendingMachineState, to define a common interface with methods for all possible user actions (e.g., insert_coin(), select_item()). Each specific stateโsuch as IdleState and HasMoneyStateโinherits from this parent. These child classes provide the concrete logic for each action, tailored to the rules of that particular state.
The key advantage of this hierarchy is polymorphism. My main VendingMachine class interacts with any state object through that common interface, allowing me to call current_state.select_item() without needing if/else logic to check which state is active. This makes my design clean, flexible, and easy to extend with new states in the future.
My design is already built on the State pattern, which is the most critical one for this problem. It lets the machine's behavior change as its state changes, like going from IdleState to HasMoneyState.
Beyond that, I would incorporate two other key patterns:
VendingMachine class. This ensures there's only one instance of the machine, preventing any conflicting states or inventories across the system.Inventory act as the subject. When stock runs low, it would automatically notify a MaintenanceService (the observer) without being tightly coupled to it.Finally, my VendingMachine class already acts as a Facade, providing a simple, clean interface (select_item(), insert_coin()) that hides the more complex internal logic of states and inventory management from the user.
Attributes: For each class, define the attributes (data) it will hold...
Methods: Define the methods (functions) that operate on the attributes. Ensure they align with the object's responsibilities and adhere to the principle of encapsulation.
from enum import Enum
from typing import Dict, Optional
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
class Coin(Enum):
PENNY = 1
NICKEL = 5
DIME = 10
QUARTER = 25
class Item:
def init(self, code: str, name: str, price: int):
self.code = code
self.name = name
self.price = price
class Inventory:
def init(self):
self.item_map: Dict[str, Item] = {}
self.stock_map: Dict[str, int] = {}
def add_item(self, code: str, item: Item, quantity: int) -> None:
self.item_map[code] = item
self.stock_map[code] = quantity
def get_item(self, code: str) -> Optional[Item]:
return self.item_map.get(code)
def is_available(self, code: str) -> bool:
return self.stock_map.get(code, 0) > 0
def reduce_stock(self, code: str) -> None:
self.stock_map[code] = self.stock_map[code] - 1
class VendingMachineState(ABC):
"""Abstract base class for all vending machine states."""
def __init__(self, machine):
self.machine = machine
@abstractmethod
def insert_coin(self, coin: Coin):
"""Handle coin insertion."""
pass
@abstractmethod
def select_item(self, code: str):
"""Handle item selection."""
pass
@abstractmethod
def dispense(self):
"""Handle dispense request."""
pass
@abstractmethod
def refund(self):
"""Handle refund request."""
pass
class IdleState(VendingMachineState):
"""State when machine is waiting for user interaction."""
def insert_coin(self, coin: Coin):
print("Please select an item before inserting money.")
def select_item(self, code: str):
if not self.machine.inventory.is_available(code):
print("Item not available.")
return
self.machine.set_selected_item_code(code)
self.machine.set_state(ItemSelectedState(self.machine))
print(f"Item selected: {code}")
def dispense(self):
print("No item selected.")
def refund(self):
print("No money to refund.")
class ItemSelectedState(VendingMachineState):
"""State when an item has been selected but insufficient money inserted."""
def insert_coin(self, coin: Coin):
self.machine.add_balance(coin.value)
print(f"Coin inserted: {coin.value}ยข ({coin.name})")
selected_item = self.machine.get_selected_item()
if selected_item and self.machine.balance >= selected_item.price:
print("Sufficient money received.")
self.machine.set_state(HasMoneyState(self.machine))
def select_item(self, code: str):
print("Item already selected. Please insert money or request refund to select a different item.")
def dispense(self):
print("Please insert sufficient money.")
def refund(self):
self.machine.refund_balance()
self.machine.reset()
self.machine.set_state(IdleState(self.machine))
class HasMoneyState(VendingMachineState):
"""State when sufficient money has been inserted."""
def insert_coin(self, coin: Coin):
# Allow more coins (will be returned as change)
self.machine.add_balance(coin.value)
print(f"Additional coin inserted: {coin.value}ยข ({coin.name}) - will be returned as change.")
def select_item(self, code: str):
print("Item already selected. Please dispense or request refund to select a different item.")
def dispense(self):
self.machine.set_state(DispensingState(self.machine))
self.machine.dispense_item()
def refund(self):
self.machine.refund_balance()
self.machine.reset()
self.machine.set_state(IdleState(self.machine))
class DispensingState(VendingMachineState):
"""State when item is being dispensed - blocks all user input."""
def insert_coin(self, coin: Coin):
print("Currently dispensing. Please wait.")
def select_item(self, code: str):
print("Currently dispensing. Please wait.")
def dispense(self):
# Already triggered by HasMoneyState
print("Dispensing in progress...")
def refund(self):
print("Dispensing in progress. Refund not allowed.")
class VendingMachine:
def init(self):
if not hasattr(self):
self.inventory = Inventory()
self.current_state = IdleState(self)
self.balance = 0
self.selected_item_code = None
def insert_coin(self, coin: Coin) -> None:
self.current_state.insert_coin(coin)
def add_item(self, code: str, name: str, price: int, quantity: int) -> Item:
item = Item(code, name, price)
self.inventory.add_item(code, item, quantity)
return item
def select_item(self, code: str) -> None:
self.current_state.select_item(code)
def dispense(self) -> None:
self.current_state.dispense()
def dispense_item(self) -> None:
item = self.inventory.get_item(self.selected_item_code)
if self.balance >= item.price:
self.inventory.reduce_stock(self.selected_item_code)
self.balance -= item.price
print(f"Dispensed: {item.name}")
if self.balance > 0:
print(f"Returning change: {self.balance}")
self.reset()
self.set_state(IdleState(self))
def refund_balance(self) -> None:
print(f"Refunding: {self.balance}")
self.balance = 0
def reset(self) -> None:
self.selected_item_code = None
self.balance = 0
def add_balance(self, value: int) -> None:
self.balance += value
def get_selected_item(self) -> Item:
return self.inventory.get_item(self.selected_item_code)
def set_selected_item_code(self, code: str) -> None:
self.selected_item_code = code
def set_state(self, state: VendingMachineState) -> None:
self.current_state = state
My design's adherence to SOLID principles stems directly from my implementation of the State Design Pattern.
This pattern naturally enforces the Single Responsibility Principle by giving each state class its own unique set of rules. It also makes the system adhere to the Open/Closed Principle, as I can add new states (like for card payments) without modifying any existing code.
Furthermore, the state hierarchy guarantees the Liskov Substitution Principle is met. My high-level VendingMachine class depends only on the VendingMachineState abstraction, not on concrete details, which satisfies the Dependency Inversion Principle. This creates a decoupled and maintainable design.
The system handles a large number of products efficiently using dictionary lookups in the Inventory. Managing a fleet of machines is also simple, as the VendingMachine class is a self-contained blueprint that can be instantiated multiple times.
Flexibility comes directly from the State Design Pattern. I can add complex new features, like credit card payments or maintenance modes, simply by creating new State classes. This requires adding new code, not modifying existing, stable code, which makes the system easy to extend without breaking it.
Try creating a class, flow, state and/or sequence diagram using the diagramming tool. Mermaid flow diagrams can be used to represent system use cases. You can ask the interviewer bot to create a starter diagram if unfamiliar with the tool. Briefly explain your diagrams if necessary...
To make my design production-ready, I see three critical areas for future improvement:
CashBox to manage a physical inventory of coins. My current change-making logic is unrealistic because it assumes an infinite supply of change.NeedsMaintenanceState and send an alert.